Saturday, February 24, 2007

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 definition

A test is not a unit test if:

  • It talks to the database
  • It communicates across the network
  • It touches the file system
  • It can't run at the same time as any of your other unit tests
  • You have to do special things to your environment (such as editing config files) to run it.


  • 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:
    1. run the test method's class setUp function, if it exists
    2. exercise the system under test (SUT)
    3. verify the SUT behaved as you expected
    4. 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?

    0 Comments:

    Post a Comment

    << Home