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.
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.
Feature | Mocking | Patching |
---|---|---|
Purpose | Simulate behavior or state of objects/functions | Temporarily replace objects/functions in their specific use context |
Focus | Emphasizes behavior and interactions | Focuses on substituting specific components during test execution |
Implementation | Typically implemented with mock libraries (e.g., unittest.mock ) | Also implemented using the same mock libraries with methods like patch() |
Isolation | Isolates a unit from external dependencies for testing | Isolates and replaces specific components temporarily |
Flexibility | Offers flexibility in simulating various behaviors | Flexible in substituting and controlling components during test runtime |
Replaces code | Replaces the behavior of the target object | Temporarily overrides the target object in its specific use context |
Behavior control | High degree of control over the mocked object’s behavior | Allows control over the replaced object’s return value and side effects |
Common Use Cases | Useful for testing complex interactions and dependencies | Ideal 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
8from 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
12import 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
56import 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
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
21import pytest
import requests
from src.cat_api import CatFact
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.
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
33from unittest.mock import Mock
def cat_fact():
"""
Fixture to create an instance of the CatFact class.
"""
yield CatFact()
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
18def 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 tests1
pytest -v tests/test_cat_api.py
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 orunittest.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()
orassert_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