Combined with dynamic typing, the lax object model of Python makes it pretty easy to write unit tests that involve mocking some parts of the tested code. Still, there’s plenty of third party libraries — usually with “mock” somewhere in the name — which promise to make it even shorter and simpler. There’s evidently been a need here that the standard library didn’t address adequately.
It changed, however, with Python 3.3. This version introduced a new
unittest.mock module which essentially obsoleted
all the other, third party solutions. The module, available also as a backport
for Python 2.6 and above, is flexible, easy to use, and offers most if not all of the requisite functionality.
Called it, maybe?
Well, with one notable exception. If we mock a function, or any other callable object:
# (production code) class ProductionClass(object): def foo(self): self.florb(...) # (test code) something = ProductionClass() something.florb = mock.Mock()
we have very limited capabilities when it comes to verifying how they were called. Sure, we can be extremely specific:
something.florb.assert_called_with('this particular string')
or very lenient:
something.florb.assert_called_with(mock.ANY) # works for _any_ argument
but there is virtually nothing in between. Suppose we don’t care too much what exact string the method was called with, only that it was some string that’s long enough? Things get awkward very fast then:
for posargs, kwargs in something.florb.call_args_list: arg = posargs self.assertIsInstance(arg, str) self.assertGreaterEqual(len(arg), 16) break else: self.fail('required call to %r not found' % (something.florb,))
Yes, this is what’s required: an actual loop through all the mock’s calls1; unpacking tuples of positional and keyword arguments; and manual assertions that won’t even emit useful error messages when they fail.
Eww! Surely Python can do better than that?
Why, of course it can! Today, I’m releasing callee: a library of argument matchers
unittest.mock. It enables you to write much simpler, more readable,
and delightfully declarative assertions:
from callee import String, LongerOrEqual something.florb.assert_called_with(String() & LongerOrEqual(16))
A loop is not necessary if we simulate
assert_called_with, but we still have to dig into
mock.callobjects to eke out the arguments. ↩