Stubbing vs. Mocking

We’ve done a lot of testing of Python systems over the past couple of years. The best approach we’ve found so far for replacing the behaviour of test objects at runtime is stubbing.

We’ve read a lot about mocking and even written mocking libraries for other languages. But there is something about the way that mocks need to be manipulated that feels distinctly un-Pythonic.

Unlike some other languages, Python allows you to access most of the internals of objects easily at runtime. Using this facility, we have developed the stub library to help with replacing functionality on functions and objects.

Separation of concerns

When you can modify objects and functions for testing purposes and confine those modifications to testing code, your production code becomes much cleaner and easier to read. You can do away with all the large if debug: blocks and keep everything clear and concise.

This problem is usually solved with complex type hierarchies providing levels of abstraction that allow developers to plug testing implementations into the pre-defined interface. What’s funny/sad about this is that it is pure duplication of effort. We already have an interface, it’s just that we need to hook into it. Most of the function definitions and current type hierarchies do not need to be further abstracted in order to support testing.

A prime example is database connector modules. Obviously, you expect this module to actually connect to a database. Equally obvious is the fact that you don’t want this module connecting to a database for every single one of it’s thousands of unit-tests. So most projects have an abstraction layer with an AbstractAccessor superclass that is then subclassed into MySQLAccessor and TestAccessor classes. This is not a terrible solution, but it does increase the cognitive overhead of dealing with the hot, or most common, path of execution.

When we write code that is clear in intention, it is easy to maintain and update. Each successive layer of abstraction we add makes this more difficult. So keeping the test code completely separate from the production code just makes sense.

This philosophy leads us to prefer code like this:

class MySQLAccessor:
        def __init__(self):
                pass

        def connect(self, user=None, pass=None, dbhost=None):
                do_something()

def myfunc(x):
        return do_something_real()

# module mylib_test
import stub

def myfunc_repl(x):
        return 5 + x

stub.stub(myfunc, myfunc_repl)

But while stubbing may seem easy when you look at replacing behaviour on instance objects, stubbing functional code turns out to be a whole ‘nother ball of wax...

But I did import it!

One of the most frustrating things when testing is needing to replace the behaviour of a function from another module.

When the function is used in multiple places in the codebase and each place uses it’s own import statement, each namespace has a binding to the original function so simple monkeypatching of the source module won’t help:

# Module module_a
import mylib

do_something(mylib.myfunc())

# Module module_b
import mylib

do_something_else(mylib.myfunc())

# Module mylib

def myfunc():
        return do_something_we_dont_want()

In this example, the naive approach to replacing the implementation of mylib.myfunc is to monkeypatch the mylib module like so:

# Module mod_mylib
import mylib

def myfunc_repl():
        return do_something_desirable()

mylib.myfunc = myfunc_repl

Unfortunately, this code must execute before the code in module_a and module_b or else they will get handles on the original myfunc before it’s definition is replaced. In order to update the code executed by all handles to the original myfunc it is necessary to modify the internal code object that the function object wraps. In this way, any modules that have already imported the function get the updated functionality.

The stub library handles this properly for both unbound functions and object methods. Allowing you to replace the above monkeypatching code with the following:

# Module mod_mylib
import stub
import mylib

def myfunc_repl():
        return do_something_desirable()

stub(mylib.myfunc, myfunc_repl)

This code can run at any time regardless of imports into other modules and will correctly update the code of myfunc so that all handles execute the new instructions.

Table Of Contents

Previous topic

API Documentation

Next topic

Unit tests

This Page