The Ultimate Guide To Using Pytest Monkeypatch with Real Code Examples
Picture this: you’re writing tests for a complex Python application, and suddenly, you hit a wall.
Your code more often than not, depends on a database, external APIs, environment variables, or intricate global settings that seem impossible to isolate or control.
You want to write fast, reliable unit tests, but every attempt to mock or patch pieces of the code leaves you in a tangled mess of complexity.
Tests are brittle, dependencies are hard or impossible to mock, and the test suite feels more like a maze than a safety net.
Sound familiar?
These struggles often stem from poor code design and deeply coupled dependencies.
While clean code principles can help mitigate these issues, the reality is that patching remains a useful tool in some scenarios.
But not all patching is created equal.
Enter Pytest’s monkeypatch
fixture: a cleaner, safer way to mock and patch during testing.
In this article, you’ll learn why and how to use the monkeypatch
fixture within Pytest to overcome common testing challenges.
From patching functions to modifying dictionaries and environment variables, we’ll explore how to use monkeypatch
and guide you with practical examples to make informed decisions about when—and how—to monkeypatch
effectively.
Let’s dive in.
What is Patching?
Patching is a testing technique used to temporarily replace parts of your code—such as functions, objects, or modules—at runtime.
Its original purpose was to isolate the unit or class under test, allowing you to focus solely on its behavior and interactions.
However, patching has often been misconstrued as a way to isolate external dependencies like APIs, databases, or configuration settings.
While patching external dependencies can simplify test setups, it risks coupling tests too tightly to implementation details.
This goes against its intended use and can lead to brittle tests that break during refactors. Properly applied, patching supports clean code and focused unit testing.
What is Monkeypatching and Why Use It?
Monkeypatching is a technique used to modify code behavior at runtime, particularly useful in testing scenarios where certain dependencies or global settings make it challenging to isolate functionality.
The monkeypatch
fixture in Pytest allows you to safely and temporarily alter attributes, environment variables, dictionary values, or even system paths for testing purposes.
This approach is useful for testing code that interacts with external systems like APIs, databases, or file systems, where direct access might be impractical or unreliable.
Monkeypatching ensures that you can mock or replace these dependencies without affecting other parts of your application.
Common Use Cases:
- Patching Functions: Replace or modify the behavior of specific functions or methods during tests.
- Environment Variables: Temporarily set or delete environment variables to simulate different configurations.
- Dictionary Items: Modify dictionary values, such as application settings, for targeted test cases.
- System Path Modifications: Adjust sys.path to control module imports.
- External Dependencies: Mock network calls, authentication layers, or delays for better test isolation.
With monkeypatching, you can test your code in controlled environments, ensuring reliable and maintainable test cases while avoiding the pitfalls of direct dependency management.
Here are some key attributes of the monkeypatch
fixture in Pytest.
Monkeypatch Attributes
setattr(obj, name, value)
: Set an attribute on an object for the duration of the test.delattr(obj, name)
: Delete an attribute from an object.setitem(mapping, name, value)
: Set a key-value pair in a dictionary.delitem(mapping, name)
: Remove a key from a dictionary.setenv(name, value)
: Set an environment variable.delenv(name)
: Remove an environment variable.syspath_prepend(path)
: Add a path to sys.path.chdir(path)
: Change the current working directory.context()
: Apply patches in a controlled scope.
Each modification is automatically reverted after the test completes, ensuring a clean test environment for each run.
Now that we’ve reviewed the basics, let’s dive in with some practical examples.
Setting Up Your Environment for Testing
Let’s set up a simple Python environment to follow along and run the examples.
- Create a new directory for your project.
1 | mkdir pytest-monkeypatch-example |
Alternatively, you can clone the GitHub repository for this article.
- Create a virtual environment.
1 | python3 -m venv venv |
- Install Dependencies.
1 | pip install -r requirements.txt |
We’ll go through a few examples to demonstrate how to use monkeypatch
in your tests.
Monkeypatching Functions
Example: Mocking Path.home to Test File Paths
Imagine you have a function that constructs the SSH directory path for the current user based on their home directory.
When testing, you don’t want the test to depend on the actual user’s home directory, as it could vary across systems. This is where monkeypatch could solve the problem.
Here’s how you can mock Path.home
using monkeypatch to ensure consistent test behavior across any system:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# contents of test_module.py
from pathlib import Path
def get_ssh_path():
return Path.home() / ".ssh"
def test_get_ssh_path(monkeypatch):
def mockreturn():
return Path("/tmp")
monkeypatch.setattr(Path, "home", mockreturn)
assert get_ssh_path() == Path("/tmp/.ssh")
# Optional: reset the monkeypatch if needed for rest of the test
monkeypatch.delattr(Path, "home")
The test ensures the function get_ssh_path
behaves as expected, regardless of the actual environment.
Monkeypatching Returned Objects: Building Mock Classes
When testing functions that call external APIs, perhaps you wish to mock the returned objects to avoid actual API calls.
Mocking is especially useful when the API response contains methods or attributes your code depends on.
Let’s demonstrate this using the MeowFacts API, which provides random cat facts.
Example: Mocking the MeowFacts API Response
Suppose we have a function that retrieves a random cat fact using the requests library.1
2
3
4
5
6
7
8
9
10
11
12# contents of src/cat_facts.py
import requests
def get_cat_fact():
"""Fetches a random cat fact from the MeowFacts API."""
response = requests.get("https://meowfacts.herokuapp.com/")
return response.json()
if __name__ == "__main__":
print(get_cat_fact())
Running the module, you should see a random cat fact printed to the console.
Mocking with Monkeypatch1
2
3
4
5
6
7
8
9
10
11
12
13# contents of test_cat_facts.py
from src.cat_fact import get_cat_fact
class MockResponse:
def json():
return {"data": ["Cats can jump up to six times their length."]}
def test_get_cat_fact(monkeypatch):
monkeypatch.setattr("requests.get", lambda x: MockResponse())
assert get_cat_fact() == {"data": ["Cats can jump up to six times their length."]}
monkeypatch.setattr
replacesrequests.get
withmock_get
.mock_get
returns an instance of MockResponse.- The
MockResponse.json()
method mimics the real API by returning a known response.
Reusing the Monkeypatch with a Fixture
If you need to mock the MeowFacts API across multiple tests, you can move the logic into a reusable fixture.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import pytest
from src.cat_fact import get_cat_fact
def mock_meowfacts_api(monkeypatch):
"""Mock requests.get to simulate the MeowFacts API."""
class MockResponse:
def json():
return {"data": ["Cats sleep 70% of their lives."]}
def mock_get(*args, **kwargs):
return MockResponse()
monkeypatch.setattr("requests.get", mock_get)
def test_get_cat_fact(mock_meowfacts_api):
result = get_cat_fact()
assert result == {"data": ["Cats sleep 70% of their lives."]}
Global Monkeypatch: Use Cases and Dangers
Global monkeypatching allows you to override or disable functionality across all tests, providing a safety net to prevent unwanted operations.
A common use case is blocking external network calls during local or CI/CD tests, or mocking default settings in production-like configurations.
Example: Preventing HTTP Requests Globally
To block all HTTP requests using the requests library, you can define a global patch in a conftest.py
file:1
2
3
4
5
6
7# contents of conftest.py
import pytest
def no_requests(monkeypatch):
"""Disable all HTTP requests by removing requests.sessions.Session.request."""
monkeypatch.delattr("requests.sessions.Session.request")
This autouse=True
fixture automatically applies to all tests, ensuring that no test makes actual HTTP calls.
Any attempt to use requests will result in an error, forcing tests to use mocks or fakes for network-dependent functionality.
Use Cases
- Enforcing Isolation: Prevent tests from accidentally interacting with external systems.
- Consistency: Ensure all tests rely on predefined responses or mocks, avoiding flaky tests due to external dependencies.
- Security: Prevent sensitive data or operations from being unintentionally triggered during testing.
Dangers and Best Practices
While global monkeypatching is powerful, it comes with risks:
- Patching Built-in Functions: Avoid patching core Python functions like open or compile, as this can disrupt Pytest internals or cause unexpected behavior.
- Breaking Third-party Libraries: Patching libraries used by Pytest or other test utilities might destabilize your test suite.
- Scope Control: Use
monkeypatch.context()
to limit the scope of a patch to a specific block of code
Global monkeypatching is best reserved for scenarios where consistent behavior across all tests is essential.
Use it cautiously, ensuring you don’t unintentionally interfere with core functionality or third-party tools.
Monkeypatching Environment Variables
More often than not, you may use environment variables to configure your application.
The monkeypatch
fixture simplifies this by allowing you to safely set or delete environment variables for the duration of a test, ensuring a clean and isolated testing environment.
Common settings like API keys, database configurations, or feature flags can be easily mocked using monkeypatch
.
Monkeypatching them helps you:
- Simulate different runtime configurations.
- Test error-handling for missing or misconfigured variables.
- Ensure tests don’t accidentally affect the global environment.
Suppose we have a function that reads an environment variable and processes it:1
2
3
4
5
6
7
8import os
def get_app_mode():
"""Fetches the application mode from the APP_MODE environment variable."""
app_mode = os.getenv("APP_MODE")
if not app_mode:
raise OSError("APP_MODE environment variable is not set.")
return app_mode.lower()
Writing a couple of tests for this1
2
3
4
5
6
7
8
9
10
11
12def test_get_app_mode(monkeypatch):
"""Test behavior when APP_MODE is set."""
monkeypatch.setenv("APP_MODE", "Production")
assert get_app_mode() == "production"
def test_missing_app_mode(monkeypatch):
"""Test behavior when APP_MODE is not set."""
monkeypatch.delenv("APP_MODE", raising=False)
with pytest.raises(OSError, match="APP_MODE environment variable is not set."):
get_app_mode()
The monkeypatch.setenv
and monkeypatch.delenv
methods allow you to simulate different environment configurations, ensuring your code behaves as expected under various conditions.
Conclusion
In this article, you explored how Pytest’s monkeypatch
fixture provides an elegant, powerful solution for overcoming challenges like mocking APIs, handling environment variables, and isolating dependencies during testing.
From patching functions and modifying dictionaries to overriding environment variables and building reusable mock classes, monkeypatch offers a versatile approach to creating reliable, isolated tests.
By understanding the basics of monkeypatching and its use cases, you can write cleaner, more maintainable tests that focus on behavior rather than implementation details.
I highly encourage you to explore the official Pytest documentation and experiment with different scenarios to deepen your understanding of monkeypatch
.
If you have ideas for improvement or like for me to cover anything specific please Email me, I’d love to hear from you.
Till the next time… Cheers!