How Mock Can Improve Your Unit Tests: Part 2

In Part 1 of this blog series, we started looking at how we can use mocking to improve our unit tests. We'll expand on that in this post.
Mock an Attribute
Another common case is mocking an attribute of an object or class. We
can use mock.patch.object
for this.
Suppose we have a function that will try to read from a file handle it has been passed, and we want to test what it does if the read fails.
def read_from_handle(fh):
try:
return len(fh.read())
except IOError:
return None
from django.test import TestCase
from unittest import mock
from ... import read_from_handle
class TestClass(TestCase):
def test_read_error(self):
fh = open("testfile", "r")
with mock.patch.object(fh, "read") as mock_read:
mock_read.side_effect = IOError("fake error for test")
retval = read_from_handle(fh)
self.assertIsNone(retval)
On line 8, mock.patch.object(fh, "read")
means that while the mock is
in effect, we're going to replace the value of the read
attribute of
the object fh
with our mock object. Again, by default,
mock.patch.object
just creates a new mock object to use.
On line 9, by assigning an exception to the side_effect
attribute of
the mock object, we indicate that when called, instead of returning a
value, the exception should be raised. Our function is supposed to catch
the exception and return None
, so we check in the usual way that its
return value is None
.
I try to use mock.patch.object
instead of mock.patch
when I can,
because it makes more sense to me when I try to figure out where to
apply the mock. But both methods have their uses.
Mock Something on a Class
If we don't have access to an object to mock something on it, perhaps because it doesn't exist yet, we may instead apply a mock to the class that will be used to create the object. We just need to be sure the mock is in effect when the object is created.
class SomeClass:
def some_function(self):
return 1
def function_to_test():
obj = SomeClass()
return obj.some_function()
from django.test import TestCase
from unittest import mock
from ... import function_to_test, SomeClass
class TestClass(TestCase):
def test_object_all(self):
with mock.object.patch(SomeClass, "some_function") as mock_function:
mock_function.return_value = 3
self.assertEqual(3, function_to_test())
By mocking some_function
on the class object, we arrange that the
instance created from it will also have some_function
be our mock
object.
How mock.patch and mock.patch.object Work
I have a mental model of how mock.patch
works that is useful for me.
I'm sure in reality it's a lot more complicated, but I imagine it does
something like this:
with mock.patch('pkg.module.name') as xyz:
run code
## which is implemented something like
import sys.modules
module = sys.modules["pkg"]["module"]
saved_value = module["name"]
mock_object = mock.MagicMock()
module["name"] = mock_object
xyz = mock_object
[ run code ]
module["name"] = saved_value
It finds the reference by name in the appropriate module, saves its value, changes it to the mock object, and when done, restores the value.
And I imagine something similar for mock.patch.object
:
with mock.patch.object(some_object, "attrname") as xyz:
run code
## which is implemented something like
saved_value = getattr(some_object, "attrname")
mock_object = mock.MagicMock()
setattr(some_object, "attrname", mock_object)
xyz = mock_object
[ run code ]
setattr(some_object, "attrname", saved_value)
This is even simpler.
Controlling the Mock Object's Behavior
We've already seen that we can assign to .return_value
on a mock
object and whenever the mock object is called, the value we set will be
returned.
For more complex behaviors, we can assign to .side_effect
:
- Assign a list of values, and each time the mock object is called, the next value in the list will be returned.
- Assign an exception instance or class, and calling the mock object will raise that exception.
- Assign a callable, and anytime the mock object is called, the arguments will be passed to the callable, and the callable's return value will be returned from the mock object.
Most of the arguments that can be passed when constructing a mock object can also be assigned as attributes later, so reading the documentation for the mock class should give you more ideas.
Determining What was Done with a Mock Object
You can assert that a mock object has been called with
mock_object.assert_called()
. It's more useful to assert that its last
call had the arguments you expect by using
mock_object.assert_called_with(*args, **kwargs)
. Or that the mock was
not called, using mock_object.assert_not_called()
.
If you don't care about some of the arguments, you can pass mock.ANY
to assert_called...
methods in place of those arguments, and the
assertion will pass regardless of what value that argument had:
mock_object.assert_called_with(1, 2, mock.ANY, x=3, y=mock.ANY)
See the mock class documentation for more variations on the same theme.
And if you want to check something that there's no built-in method to
check, you can always access mock_object.call_args_list
, which is a
list of (args, kwargs)
pairs, one for each time the mock object was
called.
NOTE: Be careful when spelling method names on mock objects, because if
you misspell one, instead of an error, the mock object will just define
a mock method by that name and return another mock object. Sometimes you
can avoid this by using the spec
argument when creating a mock object,
but that's beyond the scope of this blog series.