Illustration of a desktop computer with code on the screen

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
 1  from django.test import TestCase
 2  from unittest import mock
 3  from ... import read_from_handle
 4 
 5  class TestClass(TestCase):
 6      def test_read_error(self):
 7          fh = open("testfile", "r")
 8          with mock.patch.object(fh, "read") as mock_read:
 9              mock_read.side_effect = IOError("fake error for test")
10              retval = read_from_handle(fh)
11 
12          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.

New Call-to-action
blog comments powered by Disqus
Times
Check

Success!

Times

You're already subscribed

Times