Test reuse trick
The problem
Tests are great!
Tests that don't get run are not so great.
Slow tests don't get run (not great)
Duplication in code is not great
Unit test definition
Michael Feathers unit test definitionA test is not a unit test if:
This does not mean we don't write tests that touch the database ,etc.
We just call them functional tests or integration tests.
Violating any of these rules, usually makes tests slow and/or difficult to run.
Example
Let's say you were creating an application that needed a database.For example:
http://ciber.com/jobs
Example 1 - Separate (and duplicated) tests
First, we'll write some tests that talk to a fake database object.test_fake.py
Then we'll blindly copy the test class above and make minor changes so we can talk to a real database object.
test_real.py
Then we'll write the real database object.
real.py
Unit test framework refresher
The unittest frameworks operate in a 4 step process.for every test* method found in the test file:
- run the test method's class setUp function, if it exists
- exercise the system under test (SUT)
- verify the SUT behaved as you expected
- run the test method's class tearDown function, if it exists
test_fake.py version 1
#!/usr/bin/env python
import unittest
class FakeJobsDB(object):
def __init__(self):
self.data= {}
def get_users(self):
return self.data.keys()
def add_user(self, user_id):
self.data[user_id] = None
class TestUnit(unittest.TestCase):
def setUp(self):
self.db = FakeJobsDB()
def test_empty_db(self):
self.assertEquals([],self.db.get_users())
def test_add_user(self):
self.user_id = u"Tammy"
self.db.add_user(self.user_id)
self.assertEquals([self.user_id,], self.db.get_users())
if __name__ == '__main__':
unittest.main()
test_real.py version 1
#!/usr/bin/env python
import unittest
import real
class TestInteg(unittest.TestCase):
def setUp(self):
self.conn = real.create_db()
self.db = real.RealJobsDB(self.conn)
def test_empty_db(self):
self.assertEquals([], self.db.get_users())
def test_add_user(self):
self.user_id = "Tammy"
self.db.add_user(self.user_id)
self.assertEquals([(self.user_id,)], self.db.get_users())
if __name__ == '__main__':
unittest.main()
real.py version 1
#!/usr/bin/env python
import sqlite3
def create_db():
conn = sqlite3.connect(':memory:')
curr = conn.cursor()
curr.execute('''create table jobs (id)''')
return conn
class RealJobsDB(object):
def __init__(self,db_conn):
self.conn = db_conn
self.curr = self.conn.cursor()
def get_users(self):
self.curr.execute('''select id from jobs''')
ids = self.curr.fetchall()
return ids
def add_user(self,user_id):
self.curr.execute('''insert into jobs
(id) values ("%s")''' % user_id)
Example 2 - Reuse the tests (DRY)
Nothing needs to change in here.test_fake.py
Now let's get rid of the duplication in the tests.
test_real.py
Nothing needs to change in here either.
real.py
test_fake.py version 2
Nothing changes in here.
test_real.py version 2
#!/usr/bin/env python
import unittest
import test_fake
import real
class TestInteg(test_fake.TestUnit):
def setUp(self):
self.conn = real.create_db()
self.db = real.RealJobsDB(self.conn)
if __name__ == '__main__':
unittest.main()
real.py version 2
Nothing changes in here either.Example 3 - A slightly more complicated setup
Imagine you needed to initialize the database with an admin user.The set up that is common to both test suites is put in a separate function and then the setUp function in each suite calls the common function.
test_fake.py
Now let's get rid of the duplication in the tests.
test_real.py
Nothing needs to change in here either.
real.py
test_fake.py version 3
#!/use/bin/env python
import unittest
class FakeJobsDB(object):
def __init__(self):
self.data= {}
def get_users(self):
return self.data.keys()
def add_user(self, user_id):
self.data[user_id] = None
class TestUnit(unittest.TestCase):
def setUp(self):
self.db = FakeJobsDB()
self.setup_common()
def setup_common(self):
self.init_id = u"admin"
self.db.add_user(self.init_id)
def test_verify_db_type(self):
self.assert_(isinstance(self.db, FakeJobsDB))
def test_empty_db(self):
self.assertEquals([self.init_id],self.db.get_users())
def test_add_user(self):
self.user_id = u"Tammy"
self.db.add_user(self.user_id)
self.assertEquals([self.init_id,self.user_id,], self.db.get_users())
if __name__ == '__main__':
unittest.main()
test_real.py version 3
#!/usr/bin/env python
import unittest
import test_fake
import real
class TestInteg(test_fake.TestUnit):
def setUp(self):
self.conn = real.create_db()
self.db = real.RealJobsDB(self.conn)
self.setup_common()
def test_verify_db_type(self):
self.assert_(isinstance(self.db, real.RealJobsDB))
if __name__ == '__main__':
unittest.main()
real.py version 3
Nothing changes in here.Summary
Links
Grig's blog agiletesting.blogspot.com (with links to Titus' blog, etc.)xunitpatterns.com and now in book form
Pragmatic Programmer: Pragmatic Unit Testing: in Java with JUnit
They have a great Unit testing summary in pdf.
Buildbot (buildbot.net): Because tests that don't get run are worthless
Did I mention ciber.com/jobs?