Mocking Vs. Patching (A Quick Guide For Beginners)

As a software developer it’s not uncommon that your code depends on external systems, databases, or APIs.

So how do you write tests for code that’s heavily reliant on these systems, totally out of your control?

Perhaps you’ve heard of mocking, patching or even stubbing and have little to no idea what it means or how to use it.

Should you write mocks, or patches? What’s the difference between them? Which one should you use?

This can be quite confusing, especially if you’re new to testing.

In this article, we’ll break this down.

We’ll dive deeper into mocking and patching and explore the differences between them.

You’ll come out with a clear understanding of when and most importantly how to use mocking and patching in your tests, where needed and appropriate.

Let’s get into it.

Example Code

What You’ll Learn

By the end of this article, you’ll have

  • A clear understanding of Mocking and Patching.
  • How to decide which to use and when.
  • Essential best practices when working with mocks.

Mocking vs Patching - TL;DR

Mocking is about creating a fake version of an object that mimics the behavior of the real object.

Patching, on the other hand, involves temporarily replacing the actual implementation of a method or object during test execution, which is particularly useful for controlling external dependencies like APIs.

FeatureMockingPatching
PurposeSimulate behavior or state of objects/functionsTemporarily replace objects/functions in their specific use context
FocusEmphasizes behavior and interactionsFocuses on substituting specific components during test execution
ImplementationTypically implemented with mock libraries (e.g., unittest.mock)Also implemented using the same mock libraries with methods like patch()
IsolationIsolates a unit from external dependencies for testingIsolates and replaces specific components temporarily
FlexibilityOffers flexibility in simulating various behaviorsFlexible in substituting and controlling components during test runtime
Replaces codeReplaces the behavior of the target objectTemporarily overrides the target object in its specific use context
Behavior controlHigh degree of control over the mocked object’s behaviorAllows control over the replaced object’s return value and side effects
Common Use CasesUseful for testing complex interactions and dependenciesIdeal for controlling external dependencies like APIs during testing

Now the more detailed version with examples.

Understanding Mocking: Concepts and Examples

Mocking refers to the creation of fake objects that mimic the behavior of real objects in controlled ways enabling you to temporarily replace functions or objects under test with mock objects that return a result of your choice - fixed values, raise exceptions or side effects.

This approach enables you to write unit tests that focus on the functionality of your code without having to manage external dependencies.

Suppose you have a function send_email that you want to test, but you don’t want to actually send emails when testing.

1
2
3
# function_to_test.py
def send_email(email_address, message):
email_service.send(email=email_address, message=message)

You can test it as follows.

1
2
3
4
5
6
7
8
from unittest.mock import Mock
import function_to_test

def test_send_email():
mock_email_service = Mock()
function_to_test.email_service = mock_email_service
function_to_test.send_email("test@example.com", "Hello")
mock_email_service.send.assert_called_with(email="test@example.com", message="Hello")

In the above code, we create a mock object mock_email_service that mimics the behavior of the real email_service. We then replace the real email_service with the mock object and test the send_email function.

We’ve covered Pytest Mocking in great detail in this article.

Now that you’ve got a gist of Mocking, let’s move onto Patching and see what that is.

Understanding Patching: How It Works

Patching temporarily replaces the actual implementation of a method, function, or object with a mock during testing.

This adjustment is made at runtime, which means that the code under test does not change, but its dependencies are controlled.

Patching is particularly useful for ensuring that external systems do not affect the test outcome and that the test conditions are consistent and repeatable.

Let’s modify the previous example to use patching using the pytest-mock library.

1
2
3
4
5
6
7
8
9
10
11
12
import function_to_test

def test_send_email(mocker):
# Use mocker to patch 'email_service.send' method in the 'function_to_test' module
mock_email_service = mocker.patch('function_to_test.email_service.send')

# Call the function under test
function_to_test.send_email("test@example.com", "Hello")

# Assert that the send method was called correctly
mock_email_service.assert_called_once_with(email="test@example.com", message="Hello")

In the above code, we use the mocker fixture provided by pytest-mock to patch the email_service.send method in the function_to_test module. It automatically handles the start and end of the patching, meaning the original method is restored after the test, which helps prevent side effects on other tests.

While very similar to mocking, patching is more about replacing the actual implementation of a function or object with a mock, rather than creating a fake object.

The differences are very subtle, and the terms are often used interchangeably, but it’s essential to understand the distinction.

Practical Example

Let’s look at a more practical example to get your hands dirty.

Here’s a simple CatFact API that returns a random fact about cats.

Prerequisites

Some basics of Python and Pytest would be helpful:

  • Python (3.12 or the latest one)
  • Pytest

Getting Started

Our example repo looks like this:

1
2
3
4
5
6
7
8
9
10
.
├── .gitignore
├── pytest.ini
├── README.md
├── requirements.txt
├── tests
│ └── cat_api.py
└── src
├── __init__.py
└── test_cat_api.py

To get started, clone the Github Repo here, or you can create your own repo by creating a folder and running git init to initialize it.

Create a virtual environment and install the required packages using the following command:

1
pip install -r requirements.txt

Feel free to use any package manager you wish.

Example Code

Our example code looks something like this:

src/cat_api.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import json
import logging
from typing import Tuple, Dict
import requests

# Configure logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)

# Get a logger instance for the module
logger = logging.getLogger(__name__)


class CatFact:
def __init__(self):
"""
Method to initialize the class with base URL
"""
self.base_url = "https://meowfacts.herokuapp.com/"

def _fetch_data(self, url) -> requests.models.Response:
"""
Method to fetch data from URL
"""
return requests.get(url)

def get_cat_fact(self) -> Dict[str, any]:
"""
Method to get API status
"""
try:
response = self._fetch_data(self.base_url)
if response.status_code in (200, 201):
return {
"status_code": response.status_code,
"response": response.json(),
}
else:
return {
"status_code": response.status_code,
"response": {"ERROR": "Cat Fact Not Available"},
}
except requests.exceptions.RequestException as err:
return {
"status_code": None,
"response": {"ERROR": "Request Exception occurred"},
}

def main():
cat_fact = CatFact()
response = cat_fact.get_cat_fact()
print(response)

if __name__ == "__main__":
main()

In the above code, the class CatFact includes 2 methods - _fetch_data() and get_cat_fact().

Let’s run the code,

1
python src/cat_api.py

pytest-mocking-patching-1

Test Code - No Mocking

Let’s write some basic tests for our code,

tests/test_cat_api.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pytest
import requests
from src.cat_api import CatFact


@pytest.fixture
def cat_fact():
"""
Fixture to create an instance of the CatFact class.
"""
yield CatFact()


def test_get_cat_fact_no_mock(cat_fact):
"""
Test the get_cat_fact method of the CatFact class (No mocking or patching).
"""
response = cat_fact.get_cat_fact()
print(response)
assert response["status_code"] == 200

Here test_get_cat_fact_no_mock() is a normal test function without mocking, patching, or stubbing.

This function runs a simple test, ensuring our source code works.

pytest-mocking-patching-2

While this works fine, it’s not ideal for testing.

Why?

Because it’s dependent on the external API, which can be unreliable, slow, or even unavailable. Not to mention you can quickly rack up a bill if you’re testing a paid API.

Besides this, the API is completely out of your control and makes the whole test process slow and unpredictable.

So what’s the best solution?

Test Code - Mocking

The get_fact_cat() method in the CatFact class makes an HTTP request to the CatFact API. This method uses the _fetch_data() method to fetch data from the API (using requests.get()).

We can mock the get_cat_fact() method to return a predefined response without making an actual HTTP request.

tests/test_cat_api.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from unittest.mock import Mock

@pytest.fixture
def cat_fact():
"""
Fixture to create an instance of the CatFact class.
"""
yield CatFact()


@pytest.fixture
def mock_response():
yield {
"status_code": 200,
"response": {"data": "Cats are amazing!"},
}

def test_get_cat_fact_mock(cat_fact, mock_response):
"""
Test the get_cat_fact method of the CatFact class (With Mocking).
"""

# Create a mock for the get_cat_fact method
cat_fact.get_cat_fact = Mock(return_value=mock_response)

# Call the method
response = cat_fact.get_cat_fact()

# Assert the mock was called once
cat_fact.get_cat_fact.assert_called_once()

# Assert the expected response
assert response == mock_response

In the above code, we replace the get_cat_fact method of the cat_fact instance with a mock object that is set to return mock_response when called.

This setup ensures that the method call is simulated without executing the real logic within get_cat_fact.

Test Code - Patching

Let’s see how we can use patching to test the get_cat_fact() method.

tests/test_cat_api.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def test_get_cat_fact_patch(mocker, cat_fact, mock_response):
"""
Test the get_cat_fact method of the CatFact class (with Patching).
"""

# Use mocker to patch the get_cat_fact method of the CatFact class and get a reference to the mock
mock_get_cat_fact = mocker.patch(
"src.cat_api.CatFact.get_cat_fact", return_value=mock_response
)

# Call the method
response = cat_fact.get_cat_fact()

# Assert the mock was called once
mock_get_cat_fact.assert_called_once()

# Assert the expected response
assert response == mock_response

The above example uses the mocker fixture provided by pytest-mock to patch the get_cat_fact method of the CatFact class. It then returns the mock_response when the method is called.

Let’s run our tests

1
pytest -v tests/test_cat_api.py

pytest-mocking-patching-3

Best Practices

Let’s quickly go through some essential best practices when using mocking or patching in your unit tests.

  • Use mocks and patches for external dependencies - Focus on isolating parts of the system that interact with external services like databases, web APIs, or third-party libraries.
  • Ensure that mocks mimic the behavior of the actual dependencies they replace as closely as possible.
  • Review API and other documentation regularly to ensure that mocks remain accurate and honour the contract.
  • Use tools that automatically unpatch or restore the original state of the object after each test to prevent side effects - Pytest fixtures with setup teardown, monkeypatch fixture or unittest.mock.patch fixtures.
  • Document why a mock or patch is necessary for understanding the context and purpose of the test with descriptive names.
  • When using mocking, verify the interaction between functions or objects, ensuring that the mock object was called with expected arguments. You can use methods like assert_called_once_with() or assert_called_with() as detailed in this guide.
  • Overusing mocking or patching may lead to overly complex tests and decrease the readability and maintainability of your codebase.

Final Thoughts

That’s all for this article.

I hope you found it useful and helped clear some of the doubts in your mind about mocking and patching.

In summary, remember that mocking is about creating fake objects that mimic the behavior of real objects, while patching is about temporarily replacing the actual implementation of a method or object with a mock during test execution.

In this article, we covered the differences between mocking and patching, how to use them, and some best practices to follow when working with mocks.

We also included a practical example of a CatFact API to demonstrate how to use mocking and patching in your tests, so go ahead and try it out.

If you have ideas for improvement or like for me to cover anything specific, please send me a message via Twitter, GitHub or Email.

Till the next time… Cheers!

Additional Readings

Example Code Repo
How To Mock In Pytest? (A Comprehensive Guide)
The Ultimate Guide To Using Pytest Monkeypatch with 2 Code Examples
What Are Pytest Mock Assert Called Methods and How To Leverage Them
What is Setup and Teardown in Pytest? (Importance of a Clean Test Environment)
Mocking and Patching
Mastering Python Mock and Patch: Mocking For Unit Testing