DRY Mockery in Python Testing

by Geoff Gerrietts on May 29, 2014


Note: the code referenced in this post, along with some extra context, can be found on github. I have tried to link into the repo at each of the inline code samples, for convenience. It might be useful to refer to the repo as a whole, though, rather than just the swatches under discussion.

When I write unit tests, I often use mock objects to help me isolate the code I’m testing. I usually use Michael Foord’s mock library to make mocking out dependencies easy. For many test applications, it’s got exactly what I need. Sometimes, though, I need something a little more.

This Calls for a Little Mockery!

Mock provides a variety of efficient ways to make use of mock objects. Most of the introductory material I have read recommends using patch as a decorator, and indicates that you can also use it as a context manager if you wish. This leads to unit tests that look something like this:

class MeanTest(unittest.TestCase):
    def test_01_empty_series(self, sm):
        self.get_series = gs = sm.get_series
        gs.return_value = []
        retval = summaries.mean_for_series('foofah')
        self.assertEqual(retval, 0)

This works great on a small scale, and you could build whole unit test suites with no more technology than this.

DRY Mockery is Best

The example code is simple enough that it wouldn’t crush your soul if you opened a file and saw twenty-five unit tests just like it. But even in those scant five lines, at least two of them are repeated: we only need to set get_series one time, and most of our unit tests are going to provide a return value for get_series. We might try to refactor like so:

class RevisedMeanTest(unittest.TestCase):
    def _set_up_mock(self, series_mgr, series):
        self.get_series = series_mgr.get_series
        self.get_series.return_value = series

    def test_01_empty_series(self, sm):
        self._set_up_series_mgr(sm, [])
        retval = summaries.mean_for_series('foofah')
        self.assertEqual(retval, 0)

This also works just fine, but it’s not quite optimal. Each test still gets decorated with the patch call. If your test suite installs more than one mock object via patch, this can get extremely cumbersome — I have worked with some test suites that have patched in five or six mock objects. The stack of decorators can take up more lines than the actual test body!

The False Promises of setUp

The beauty of the decorator solution is that it applies the patch before your test runs, and then it removes the patch when the test has concluded. The problem with the decorator approach is mainly that we copy/paste a big stack of decorators onto each method, and that the decorator does not permit us to set up our mocks properly.

The unittest library has a setUp and tearDown feature that seems to fit what we’re after. We can apply the patch in the setUp, and remove the patch in the tearDown. We can also do extra setup work in the setUp method, factoring that extra code out of the test methods.

In practice it’s not quite as elegant as it sounds. Firstly, it’s a little complicated to use these methods. Replicating the work the decorator does takes a few lines of code:

class SettingUpAMeanTest(unittest.TestCase):
    """ Using setUp and tearDown to set up mocks
    def setUp(self):
        self.get_series = m = Mock(return_value=series)
        sm = Mock(get_series=m)
        self.smpatch = patch.object(summaries, 'SeriesManager', sm)

    def tearDown(self):

    def test_01_empty_series(self):
        self.get_series.return_value = []
        retval = summaries.mean_for_series('foofah')
        self.assertEqual(retval, 0)

So no real savings in lines of code for our sample, but you can easily envision a test class with a dozen test methods, and at that point the savings will really add up. But since setUp and tearDown do the same thing for every test on your object, you are almost certain to require additional per-test setup. In our sample, that’s the line that sets self.get_series.return_value = []. In a less straightforward test case, it could be tens of lines of configuration.

Introducing PatchingTestCase

We’re almost refactored, but before we get there, I’d like to scroll back a little ways. When we moved the patching into setUp, we traded a one-line decorator call in for three lines of patch setup and teardown. That’s not a terrible amount of overhead, but it lends itself quite well to abstraction. The code to abstract it is in the PatchingTestCase class:

class PatchingTestCase(unittest.TestCase):
    def patch(self, target, attribute, obj=None):
        if obj is None:
            obj = mock.Mock()
        patched = mock.patch.object(target, attribute, obj)
        obj = patched.start()
        return obj

There’s nothing magical about this code; you could imagine deriving it yourself after a few rounds of writing it as a one off. And in fact, that’s exactly how I came up with it, at least twice now. This patch method works very much like the @patch.object decorator, but in method form. This allows us to apply patches at will, rather than just around functions, or just around blocks.

A Small Concession

We’re left one last problem to solve, which is that not all tests on a given class will necessarily have exactly the same mock setup. In our sample code, the principal difference is in the series data we return. Real-life examples are often substantially more complex. Sometimes, though, you only need the presence of a mock for all your tests to run, and no special behavior. In those cases, setUp will do quite nicely! But for everyone else, read on.

In our campaign to reduce the number of repeated lines of code, we have made significant progress, first centralizing the setup logic, then the decorators, then the patching infrastructure. But now we must give some ground. In order for each test to customize its environment, each test will need to make a method call. Check it out:

class RefactoredMeanTest(testcase.PatchingTestCase):
    def _patch_series_manager(self, series):
        self.get_series = m = Mock(return_value=series)
        sm = Mock(get_series=m)
        return self.patch(summaries, 'SeriesManager', sm)

    def test_01_empty_series(self):
        retval = summaries.mean_for_series('foofah')
        self.assertEqual(retval, 0)

In this example, our _patch_series_manager method sets up all the mocks and installs them. In the sample repo, you can see examples of additional tests that use this same method to patch in a different return value — and even one integration test that simply does not install the patch.


Monitor Python in Production

Once you've gotten your tests in line, see exactly how everything is working in production. Deploy AppNeta and see! Create Free Account