Unit Testing with Python’s Patch Decorator

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.

References

https://docs.python.org/3/library/unittest.mock.html

Leave a Reply

Your email address will not be published. Required fields are marked *