If you are new to mocking in Python, using the unittest.mock
library can be somewhat intimidating. In this post, I’ll walk you through some common methods of using the patch
decorator. The patchers are highly configurable and have several different options to accomplish the same result. Choosing one method over another can be a task. I’m going to simplify this for you by demonstrating my common approaches to patching. Understanding these techniques will cover the majority of the mocking needs you’ll face in day to day Python coding. Let’s start off with an example class to test.
Before we begin
I’m assuming you already have a good understanding unit testing, so I won’t cover unit testing fundamentals here. If you want to follow along, setup python and pip install mock
. The source code used in this post can be downloaded here.
What are we testing?
from src.api import school as school_api
class Student(object):
def __init__(self, student_name, school_name):
self.student_name = student_name
self.school_name = school_name
def get_details(self):
school = school_api.get_school(self.school_name)
student_details = {'student_name': self.student_name, 'school': school}
return student_details
The above class is pretty straight forward. It’s a Student
class that takes the student name and school name as constructor args. The Student
has a get_details
method that invokes an API to get details about the given school. We’re going to test this method and use patch
to mock out the API call.
TestCase Setup
Let’s create a simple TestCase
and create a mock function for the API call. The School API takes a school_name
and returns a dict
containing the given school_name
, along with teacher_count
and student_count
. We’ll create a function that has the same method signature as the API and returns some mock data as the expected result.
import mock
import unittest
from unittest import TestCase
from src.student import Student
def mock_get_school(school_name):
return {"school_name": school_name, "student_count": 100, "teacher_count": 8}
class StudentTest(TestCase):
def setUp(self):
self.student = Student("Tester", "Test High")
mock_get_school()
is what we will have our patcher inject whenever it encounters the school_api.get_school()
method during testing. This way, we can test the Student
class without attempting to make remote calls. Before we patch, let’s setup our test method.
Test method
def test_get_details(self):
student_details = self.student.get_details()
assert student_details["student_name"] == self.student.student_name
assert student_details["school"]["school_name"] == self.student.school_name
assert student_details["school"]["student_count"] == 100
assert student_details["school"]["teacher_count"] == 8
The test_get_details
method will use the student
that we setup during initialization of our test. After invoking student.get_details()
we will run some assertions to make sure we get back what we expect. If all goes right, the class under test will be injected with our mock and returning the expected mock data.
Python Patch Decorator
Approach #1 – Simple replacement
The first approach is simple and you’ll likely have many use cases for it. We are going to configure the decorator to target the school api and give it a method to replace the real API call. The first argument to patch
will be the lookup path of the api method. This will be the same imported path used in your class under test. The second argument will be the mock method we created above. The method is just replacing the target object, no mock
instance is ever created.
@mock.patch("src.api.school.get_school", mock_get_school)
Here’s what the final test method will look like.
@mock.patch("src.api.school.get_school", mock_get_school)
def test_get_details(self):
student_details = self.student.get_details()
assert student_details["student_name"] == self.student.student_name
assert student_details["school"]["school_name"] == self.student.school_name
assert student_details["school"]["student_count"] == 100
assert student_details["school"]["teacher_count"] == 8
This was pretty straight forward and honestly, a simple replacement will often meet your needs. The API is replaced with the mock function whenever the API is called during your test. Now you can test the Student class without needing the remote service.
Pros
- Simple
- No extra code outside of the decorator
Cons
- Your mock function cannot be a method on the TestCase (unless it’s static)
- Your function can’t (easily) respond differently to subsequent calls
- No mock was created
- There’s no object to inspect with follow-up assertions (how many times was it called)
Approach #2 – Generated mock arguments
In the second approach, we will configure the decorator so it gives you some benefits that you don’t see in the first approach. This time, we only pass the target path to the patch
decorator. When we do this, the decorator will generate a MagicMock
object and pass it as a parameter to our TestCase
method. By the way, you’ll get an AsyncMock
if mocking an async class.
@mock.patch("src.api.school.get_school")
def test_get_details_2(self, mock_school_api):
mock_school_api.side_effect = mock_get_school
student_details = self.student.get_details()
assert student_details["school"]["school_name"] == self.student.school_name
assert student_details["school"]["student_count"] == 100
assert student_details["school"]["teacher_count"] == 8
mock_school_api.assert_called_once()
What’s happening here is patch
created a mock object and passed it as an argument to our test method. With a reference to the mock, we can configure its behavior and inspect after invocation. The argument can be whatever name you give it. I’m using mock_school_api
, which will be an instance of MagicMock. It has a side_effect
property which I configure to call our mock function when invoked. Because we have the mock instance, we can run assertions on it, or configure it to respond dynamically to subsequent calls. Additionally, the side_effect can be an external function or a method defined on the TestCase
class itself.
Pros
- More flexibility
- Allows for dynamic configurations
- Explicit configuration of the mock
- You have a reference to the mock for follow-up assertions
Cons
- Additional complexity
- More lines of code
- Every mock will add to your parameter list
- Potential config duplication for repeated test scenarios
Approach #3 – External configuration
The biggest drawbacks with approach 2 are the extra params given to your test method and the configuration step (setting the side_effect, return_value, etc.), which happens in your test. But is this really that bad? If you only have a few tests, no, it’s not that bad. When you have many happy/unhappy/exception paths, the duplication of params and configuration will be evident. We can, however, avoid duplication of the config by externalizing it. To do so, pass the patcher a config (**kwargs) as the last parameter to the decorator.
PATCH_CONFIG = {'side_effect': mock_get_school}
@mock.patch("src.api.school.get_school", **PATCH_CONFIG)
If you have many test with the same mock configurations, this is solid approach.
Pros
- All the same as before with no configuration duplication
Cons
- None really
- You still get arguments passed to your test methods, but that only sucks if you have many mocks in one method (don’t do that)
Approach #4 – Explicit mock object creation
A fourth approach is a somewhat of a hybrid approach. Here, we can create a MagicMock
explicitly and pass it as a second argument to patch
. Doing this, it allows us to have a reference to the mock for follow-up inspections (without having it passed as an argument to the test method). We also can configure the mock in one place and keep our test method free of duplicated configurations when we have repeated test. Here’s the entire test class.
import mock
import unittest
from unittest import TestCase
from src.student import Student
def mock_get_school(school_name):
return {"school_name": school_name, "student_count": 100, "teacher_count": 8}
class StudentTest(TestCase):
mock_school_api = mock.MagicMock(side_effect=mock_get_school)
def setUp(self):
self.student = Student("Tester", "Test High")
StudentTest.mock_school_api.reset_mock()
@mock.patch("src.api.school.get_school", mock_school_api)
def test_get_details_4(self):
student_details = self.student.get_details()
assert student_details["school"]["school_name"] == self.student.school_name
assert student_details["school"]["student_count"] == 100
assert student_details["school"]["teacher_count"] == 8
StudentTest.mock_school_api.assert_called_once()
if __name__ == "__main__":
unittest.main()
Here, we explicitly create the mock_school_api
as a MagicMock
. It’s a static member of the class so it can be used on the decorator. Notice the setUp
method. We call reset_mock()
on our mock instance each test run. This is very important so each test has a fresh mock to start with. We explicitly created the mock so we must maintain it. The patchers role is just the injector. It replaces the target object with the mock we created and does nothing more with it.
Pros
- You have the same flexibility of being passed a mock instance without the extra params on each test method
- You can configure the mock in one place reducing duplication
- You have a mock instance for follow-up assertions
Cons
- You have to manage the mock instance yourself
- The mock retains state across each test if you don’t reset it properly
As an alternative, you can explicitly create a mock without using the decorator. The patcher can be called as a function directly on your target path. You’ll still have to maintain the mock’s state yourself by starting and stopping the patcher (setUp and tearDown).
patcher = patch('src.api.school.get_school')
mock_school_api = patcher.start()
# invocations
# assertions
patcher.stop()
Class Decorators
patch
can also be used as a class level decorator. With using class decorators, you will not be allowed to pass in mock functions that exists inside the class you are decorating. Usually this method is used if you have several of the same kinds of test methods and you need the exact same mocks for each. The patchers will recognize all methods starting with ‘test’ as methods that needs mock arguments. A mock will be created for each patch
you have on the class and passed to every eligible test method.
Summary
This post highlighted some of the common approaches for mocking in Python. The unittest.mock
library is highly configurable and allows you to mock methods and objects in many more ways than what I’ve outlined here. Sure, with so many options, the features can seem overwhelming initially. With that being said, once you’ve conquered the basics, I encourage you to explore the documentation. There, you will find a comprehensive list of all available mocking techniques and configurations. With what you’ve learned here, I’m confident you’re headed in the right direction. Thanks for reading.