Asserting Function Calls in Python
One of the nicest features of Python is “duck typing”, which means you don’t need to create interfaces to allow you to swap out implementations. Instead you simply create a different object that has the functions you need.
One really powerful use of this is in unit testing, allowing you to create lightweight replacements for dependencies without the need for a powerful mocking framework. Having said that, sometimes you need to be able to do things like checking that a function was called on an existing object. I asked about this on StackOverflow, and got a variety of different approaches to this problem.
Thanks to another feature of Python, sometimes called “monkey patching” you can take any object and replace an existing function with your own. This is obviously very powerful (and potentially dangerous) but it opens up all sorts of possibilities.
Here’s an example of monkey patching to replace the existing implementation of
MyFunc with a lambda expression that simply counts how many times it was called.
def testMyFunc(): obj = MyObject() calls = 0 obj.MyFunc = lambda: calls += 1 # DoSomething should call MyFunc DoSomething(obj) assert calls == 1
To take this one step further, we might wish to still call through to the original implementation of
MyFunc. We can simply this by creating a helper class:
class MethodCallLogger(object): def __init__(self, meth): self.meth = meth self.CallCount = 0 def __call__(self, *args): self.meth(*args) self.CallCount += 1
This class will call through to the original function, as well as count how many times it was called. The
__call__ function is a way of allowing a class to be called as though it were a function. The
*args syntax simply lets us support functions with multiple parameters. These could then be saved into a list and made available to the unit test if necessary. Here’s our first example again, using the
def testMyFunc(): obj = MyObject() logger = MethodCallLogger(obj.MyFunc) obj.MyFunc = logger # DoSomething should call MyFunc DoSomething(obj) assert logger.CallCount == 1