Mocks
Code usually has some dependencies on other parts of the application or external resources such as REST APIs or databases. To make sure our code is working correctly, we want to test the logic isolated in a unit test. Isolation is needed to guarantee our tests are fast, reliable, and reproducible. Dependencies on code can be hard to set up and slow to execute; a dependency on an external REST API or database might not be available during test execution or might return variable data. For example, if we like to test a function that syncs the frame range of a shot in a DCC with Shotgun, Shotgun might not be available, and the test would fail. Since we want our tests to be reliable, this is not an option. In some cases, we also don’t want to modify external resources. Imagine testing a submitter to a cloud rendering service. We don’t want to create a job every time the test is executed, since this costs money.
Mocks can help us solve this issue. If we use mocks, we can isolate our test from external dependencies and only test our business logic quickly. We exchange external dependencies with mocks, we can control the mocks’ behavior matching our needs, and verify method calls on the mock objects.
Mocks in Python
To use mocks, we will use a mocking framework. In Python, this is the mock framework. The mock framework ships with Python 3.3, in Python 2.7, we have to install it using pip:
The core class of the mock framework is MagicMock
.
MagicMock
acts as the replacement for external dependencies.
We can control the way MagicMock
behaves.
Let’s take a look how to create a mock for a function:
from mock import MagicMock
some_function = MagicMock()
some_function.return_value = 3
print(some_function())
This will print 3.
Sometimes, we need to control the return value of the mock and some side effects like raising an exception or calling another function.
We can use the side_effects
attribute for that.
If we provide an exception, the exception will be thrown at runtime once the mock is called:
from mock import MagicMock
some_function = MagicMock()
some_function.side_effect = ValueError
some_function()
This will raise a ValueError
.
If we provide a function, the function is called once the mock is called. It will be called with the same args as the mock:
from mock import MagicMock
some_function = MagicMock()
some_function.side_effect =lambda x: print(x)
some_function(3)
This will print 3.
We can also pass return_value and side_effects as constructor arguments:
from mock import MagicMock
some_function = MagicMock(return_value=3)
# or
some_other_function = MagicMock(side_effect=ValuError)
In some cases, we need to verify that a mock was called or was called with some expected arguments.
A MagicMock
provides some easy methods to do that:
m = mock.MagicMock()
# verify mock was called
m.assert_called()
m.assert_called_once()
# verify mock was not called
m.assert_not_called()
# verify mock was called with args
m.assert_called_with(1)
For more, see the documentation of the MagicMock
class
Objects
We know how to create a mock for a method, but also have to create mocks for classes. To create a mock for a single function of a class, we can exchange it with a mock:
class MyClass(object):
def some_method(self):
pass
def some_other_method(self):
pass
c = MyClass()
c.some_method= MagicMock(return_value=3)
But most of the time, we want to mock the whole class.
For that, we can use a MagicMock
instance.
If we try to access any attribute of a MagicMock
instance, we get back another MagicMock
instance automatically:
c = MagicMock()
c.our_method_to_mock.return_value=3
But this also means if we misspell an attribute name, our tests do not work as expected, which’s hard to detect:
# want to create a MyClass mock
c = MagicMock()
c.som_method.return_value=3
This would return a MagicMock
instance for some_method
and not 3 as expected.
We can use the spec argument of the MagicMock
constructor, and MagicMock
will use this as a reference:
class MyClass(object):
def some_method(self):
pass
# want to create a MyClass mock
c = MagicMock(spec=MyClass)
c.som_method.return_value=3
Accessing an attribute which is not part of the spec class throws an AttributeError
at runtime.
Getting mocks into tests
We can create mock functions and classes, and now we need to replace the real instances in tests.
We could do this manually, but this has some drawbacks.
After a test, we need to remove the mocked instances and have to put real instances back in place.
Otherwise, all code running after this test still uses the same mock instances and can produce weird results.
Luckily, the mock framework already provides a solution for this: patch
.
patch
allows us to replace classes or methods with mocks and puts the real classes or methods back afterward.
patch
is available as a decorator or context manager.
We pass the object to mock as a string with the full module path:
from mock import patch
@mock.patch('tests.test_dao_pattern_simple.MyClass.some_other_method')
@mock.patch('tests.test_dao_pattern_simple.MyClass.some_method')
def test_some_method(mock_some_method, mock_some_other_method):
mock_some_method.side_effect = ValueError
[...]
There are also patch.dict
and patch.object
available.
patch.dict
allows you to exchange the value for a given key in a dictionary with a mock and patch.object
exchange some object attribute with a mock:
from mock import patch
@patch.object(MyClass, 'some_method')
def test_some_method(mock_some_method):
mock_some_method.side_effect = ValueError
[...]
my_dict = {'my_key': 3}
@patch.dict(my_dict , {'newkey': 42}
)
def test_some_dict():
assert my_dict['my_key'] == 42