Mocking objects in Python unit tests
The seamless integration of doctest, Nose, Sphinx and MiniMock means that taking a little more time to write your Python doc strings can give you testable documentation, full of examples, in HTML or LaTeX markup, and main-line unit test coverage “for free”.
The bon mariage between these agile tools has worked so well for us that when it came to extending test coverage up to 100% using a full Nose test suite, we were really pining for the painless mock objects that MiniMock gives you.
MiniMock works by printing out your code’s actual usage of mock objects so that it can be compared with the expected usage you specify in the doc string. For example, this function reads a URL and writes it to a file-like object:
import urllib def write_url(url, out_file): """ Example:: >>> from minimock import mock, Mock >>> mock('urllib.urlopen', returns=Mock('urlopen_result')) >>> write_url('http://webmynd.com', Mock('out_file')) #doctest: +ELLIPSIS Called urllib.urlopen('http://webmynd.com') Called urlopen_result.read() Called out_file.write(None) <Mock ... out_file> """ page_content = urllib.urlopen(url) out_file.write(page_content.read()) return out_file
The supplied doctest shows a couple of different mocking methods, and also doctest’s invaluable ELLIPSIS option, which allows for fuzzy matching of the expected output.
When writing unit tests for this method, rather than a single simple doctest, there are two problems.
- there’s no convenient way to track the usage of MiniMock-ed objects
- the fuzzy matching tools in doctest aren’t particularly conveniently exposed for unit test usage
Tracking MiniMock usage
To track the usage of mocked objects, we subclass minimock.Printer to store the console output in a StringIO object, rather than printing it to sys.stdout:
class TraceTracker(Printer): def __init__(self, *args, **kw): self.out = StringIO() super(TraceTracker, self).__init__(self.out, *args, **kw) self.checker = doctest.OutputChecker() self.options = doctest.ELLIPSIS self.options |= doctest.NORMALIZE_WHITESPACE self.options |= doctest.REPORT_UDIFF def check(self, want): return self.checker.check_output(want, self.dump(), optionflags=self.options) def diff(self, want): return self.checker.output_difference(doctest.Example("", want), self.dump(), optionflags=self.options) def dump(self): return self.out.getvalue()
The check() method uses doctest’s OutputChecker to compare the observed and expected mock usage, while diff() returns a human-readable comparison of the observed and expected mock usage.
The basic idea is to store up the messages MiniMock would have printed in a convenient container, and provide some utilities to interrogate those messages.
Matching MiniMock usage
The TraceTracker class shown above already gives us all the functionality we need – all that is required is a convenient utility function:
def assert_same_trace(tracker, want): assert tracker.check(want), tracker.diff(want)
This function allows us to check the mock objects are being used as we expected, and prints out a human-readable diff of the expected and observed usage if applicable.
As a concrete example, I’ll convert the doctest for the write_url function to a Nose-style unit test:
def test_write_url(): tt = TraceTracker() mock('urllib.urlopen', returns=Mock('urlopen_result', tracker=tt), tracker=tt) write_url('http://webmynd.com', Mock('out_file', tracker=tt)) expected_output = """Called urllib.urlopen('http://webmynd.com') Called urlopen_result.read() Called out_file.write(None)""" assert_same_trace(tt, expected_output)
The definition of the expected MiniMock usage (called expected_output here) can feel a little clunky, but in our experience, these definitions are quite often common between test cases, so can be defined once and shared.
MiniMock is great for quickly faking out fairly complex external dependencies, with little, if any, compromise on the rigour of your tests. By adapting its usage for unit tests, as described here, you can have all that convenience and power in your more exhaustive test suites.
The code given above is available as MiniMockUnit on PyPI.
Filed under: technical |
Tags: agile, minimock, python, testing, unit test