Why aren't composable test helpers a thing?

November 12, 2013 at 02:20 PM | Python | View Comments

Often test cases require a few stateful "things" to happen during each run, which are often configured in the setUp and tearDown methods of the test case.

For example, some common "things" in applications I've worked on are:

  • Capturing email messages sent during the test case
  • Capturing log messages emitted during the test case
  • Mocking Redis connections
  • Mocking the application's job queue

And the implementation often looks something like this:

class MyRegularTestCase(TestCase):
    def setup(self):
        self.mail = setup_mock_mail()
        self.logs = setup_mock_logger()
        self.redis = setup_mock_redis()
        self.jobs = setup_mock_job_queue()

    def teardown(self):
        self.mail.teardown()
        self.logs.teardown()
        self.redis.teardown()
        self.jobs.teardown()

    def test_foo(self):
        foo()
        self.mail.assert_message_sent("hello, world")
        self.logs.assert_logged("hello, world")
        self.redis.assert_list_contains("foo", "bar")
        self.jobs.assert_job_queued("some_job")

And that is a best case example; most of the time test code doesn't use high-level mocking objects, and setup code is copy+pasted between test classes.

This makes me wonder: why aren't composable test helpers a thing?

For example, a log capturing test helper might look something like this:

class LogCapture(object):
    def __init__(self, logger_name=''):
        self.logger = logging.getLogger(logger_name)

    def setup(self):
        self.records = []
        self.logger.addHandler(self)

    def teardown(self):
        self.logger.removeHandler(self)

    def emit(self, record):
        self.records.append(record)

    def assert_logged(self, message):
        for record in self.records:
            if message in record.getMessage():
                return
        raise AssertionError("No log message containing %r was emitted" %(message, ))

And this sort of composable helper could be used with a TestCase like this:

class MyBetterTestCase(ComposableTestCase):
    mail = MailCapture()
    logs = LogCapture()
    redis = MockRedis()
    jobs = MockJobQueue()

    def test_foo(self):
        foo()
        self.mail.assert_message_sent("hello, world")
        self.logs.assert_logged("hello, world")
        self.redis.assert_list_contains("foo", "bar")
        self.jobs.assert_job_queued("some_job")

I'll be building this kind of composable test helper into my application, and if it works well, I'll release a library.

Permalink + Comments