Testing Python with Mock

Preamble

In the past few months I've been fortunate enough to write code using TDD. There a brief outline of my experiences using python Mock library help with adding test coverage for the code which is hard to test.

Lessons learned from using mock

Mocking internals of your application is hard, mostly because of dependencies on the other parts of the program, in this case object should be mocked if there is no other choice -- a much better approach is to just create object instances, this is especially true when creating database-backed objects. Integration tests of real database-backed object will verify application logic, as well as database relationships.

Mocks, however, are perfect when faking third party API responses -- there's no application interdependencies to be aware of, the test suite allows to execute code as is, without relying on third-party services or sending any data out, it codifies third party API responses as they are used by your application -- the latter point is especially helpful when a third party API changes between the releases, and any unexpected errors raised after an upgrade, can be quickly identified.

Knowing where to inject mock object

The most challenging part of mocking an object is to figure out where to inject a mocked object. The object needs to be mocked whenever its imported and used an not whenever it's defined. This makes sense because the purpose of the test is to check particular code path with a mock and not to mock that object everywhere. However, my initial assumption was to simply copy import statement from a source, which led to a lot of confusion.

In the following example, of guessing a number between 0 and 10. If the code was saved in a file main.py with import from random.randint

def guess():
    from random import randint
    print("Guessed {}".format(randint(0, 10)))

Mock setup for this case will have to inject randint mock object into main program and not rand library.

from mock import patch

with patch('main.randint') as rnd_mock:
    from main import guess
    guess()
    print(rnd_mock)

Debugging objects that don't cooperate with mock

The second-most challenging thing about mocking objects is timing -- especially the cases where an original instance of the object gets called before mock patch ever has a chance to replace the object.

from mock import patch
from foo import bar

bar.setup()

with patch('foo.bar.run'):
    bar.run()

In this particular case, going against pythonic way of importing code, keeping all the imports at the top of the file, is justified as this solves the problem, running import within patch section will do the trick.

from mock import patch

with patch('foo.bar.run'):
    from foo import bar
    bar.setup()
    bar.run()

In general there is no easy solution to this kind of problem, but a useful debugging technique would be to put a debug point before with declaration, just inside with declaration, and just before the declaration of the object to be mocked. Tracing the order of calls would give a good clue on what's wrong with the mock setup.

Most of the time, the problem is cause by the wrong assumption about the order of imports.

Mocked method call vs. mocked property

There's a difference between calling a mocked property and a method -- it's possible to just assign the value to a property, however for a function needs to be assigned the return value to the property return_value of that mock.

from mock import MagicMock
def print_property():
     myobj = MagicMock()
     myobj.myprop = 3
     print(myobj.myprop)

def print_method():
    myobj = MagicMock()
    myobj.my_method.return_value = 5
    print(myobj.my_method())

In the case of mocking methods calling other methods, return_value can be skipped until the very last method in the chain

from mock import MagicMock
def print_chained_method():
     myobj = MagicMock()
     myobj.a().b().c().last_method.return_value = 42
     print(myobj.a().b().c().last_method())

Mock helper methods

The following helper methods are useful when examining mocks state of how many times and with which parameters a mock was called.

See mock documentation for more info -- https://docs.python.org/3/library/unittest.mock.html

method calls -- a method for tracking any calls to mock objects as well as calls to their methods and attributes.

i.e. The object hasn't been called

>>> mailer.method_calls
>>> []

A constructor has been called with a 'Message' object

>>>mailer().method_calls
>>>[call.send(<pyramid_mailer.message.Message object at 0x7fb9cae06e90>)

call_args -- the arguments the mock was last called with. This method will only return arguments from the last call. To get arguments from all calls, use call_args_list.

call_args_list -- list of the calls made to the mock objects in a sequence. Useful for figuring out the order of calls.

call_count -- the number of times mock object was called

mock_calls -- records all calls to the mock object, its methods, magic methods and return value mocks.

social