Pytest: mock it, mock it!

Andrey Maslennikov
3 min readFeb 12, 2021

When refactoring/updating legacy without tests, you will find yourself writing these missing parts. Or quit the job. Or die under pile of bugs.

Most likely the legacy will be hard to test: monolithic architecture, lots of dependencies (including hidden ones). To test something you end up testing _everything_.

Mocks help isolating code under tests from other parts: the most obvious example would be passing log lines instead of entire real file for testing parsing functions, or passing synthetic data instead of making real web requests.

During the last few month, I’ve written more unit tests then I did for many years. And here is the summary of my experience with using mocks in unit tests. Let’s talk Python.

Manual monkeypatch

Here is the thing: in Python you can easily replace any function implementation in place:

# mock.pyclass Foo:
def foo(self):
return "foo"
if __name__ == "__main__":
obj = Foo()
print(obj.foo())
obj.foo = lambda: "no foo"
print(obj.foo())

output:

$ python ./mock.py
foo
no foo

This is a very nice trick: simple, no dependencies required. The problem with it is the score: once changed, all call to foo() will be replaced with this new implementation during whole object life. If this replacement is applied to the class instead of instance, all objects inside this process will inherit this behavior.

# here we replace class' implementationdef f(*args, **kwargs):
return "bar"
Foo.foo = f

Back to the problem: if not all tests need this replaced behavior, tests will be turning it on and off all over the code making it is very hard to read.

unittest.mock

Check number of calls

The simplest mock can fully replace the mocked object and also introduce very useful features:

# mock.pyfrom unittest.mock import Mockclass Foo:
def foo(self):
return "foo"
def bar(self):
foo = self.foo()
return f"bar + {foo}"
if __name__ == "__main__":
obj = Foo()
print(obj.bar())
obj.foo = Mock()
obj.foo.return_value = "mocked"
print(obj.bar())
print(f"Number of foo() calls: {obj.foo.call_count}")

output

$ python ./mock.py
bar + foo
bar + mocked
Number of foo() calls: 1

Here we replace the return value of the foo() which is optional. We also get information about number of calls to this function. And if test checks that a function was called only once, it can be asserted in test with predefined obj.foo.assert_called_once().

One-time patch

Standard unittest.mock offers also a context aware patching function.

# mock.pyfrom unittest.mock import patchclass Foo:
def foo(self):
return "foo"
def bar(self):
foo = self.foo()
return f"bar + {foo}"
if __name__ == "__main__":
obj = Foo()
print(obj.bar())
with patch("__main__.Foo.foo") as mock:
print(obj.bar())
print(obj.bar())
with patch("__main__.obj.foo") as mock:
print(obj.bar())
print(obj.bar())
with patch.object(obj, "foo") as mock:
print(obj.bar())
print(obj.bar())

output

$ python ./mock.py
bar + foo
bar + <MagicMock name='foo()' id='140138853043792'>
bar + foo
bar + <MagicMock name='foo()' id='140138853108704'>
bar + foo
bar + <MagicMock name='foo()' id='140138852628752'>
bar + foo

Three ways to mock foo():

  1. Mock Foo’s method.
  2. Mock Foo’s instance method.
  3. Mock instance’s method using patch.object().

Notice that mock’s effect disappears when out of with context which makes this patching very local. mock’s features like number of calls and return value can also be used:

with patch.object(obj, "foo") as mock:
mock.return_value = "with mocked"
print(obj.bar(), f"num of call: {mock.call_count}")

Mocking private attributes

In python it is possible to access private attributes in a hacky way. So it is possible to mock them.

from unittest.mock import patchclass Foo:
def show_private(self):
return self.__private()
def __private(self):
return "private"
if __name__ == "__main__":
obj = Foo()
print(obj.show_private())
with patch.object(obj, "_Foo__private") as mock:
mock.return_value = "mocked private"
print(obj.show_private())

output

$ python ./mock.py
private
mocked private

Ultimate pytest example

Here is complete sample of the shown approaches combined with pytest’s fixtures. Notice monkeypatch methods and usage. It is the same what patch() does.

# mock.pyimport pytest
from unittest.mock import Mock, patch
class Foo:
def foo(self):
return "foo"
def bar(self):
foo = self.foo()
return f"bar + {foo}"
class TestFoo:
# this is a special pytest method called after object creation
def setup_method(self):
self.obj = Foo()
@pytest.fixture(autouse=False)
def mock_foo(self):
self.obj.foo = Mock()
@pytest.fixture(autouse=False)
def monkeypatch_foo_return_caps(self, monkeypatch):
monkeypatch.setattr(self.obj, "foo", lambda: "FOO")
def test_one(self, mock_foo):
self.obj.bar()
self.obj.foo.assert_called_once()
def test_two(self):
obj = Foo()
with patch.object(obj, "foo") as mock:
mock.return_value = ""
res = obj.bar() assert mock.call_count == 1 assert res == "bar + " def test_three(self, monkeypatch_foo_return_caps):
assert self.obj.foo() == "FOO"

output

$ python -m pytest -vvx ./mock.py
...
mock.py::TestFoo::test_one PASSED [ 33%]
mock.py::TestFoo::test_two PASSED [ 66%]
mock.py::TestFoo::test_three PASSED [100%]

This is the end of the mocking cheatsheet.

--

--