Pytest: Mocking Objects and Classes
Mocking entire classes and objects is a crucial skill for unit testing complex code. When a function instantiates a class or calls methods on a passed-in object, you need a way to control that object's behavior without executing its real logic. This article will cover advanced mocking techniques, focusing on how to mock classes and instance methods using both the standard unittest.mock
library and the more convenient pytest-mock
plugin. We will also clarify key distinctions between different types of test doubles.
Mocking a Class
When your code instantiates a class, you can use patch
to replace the entire class with a mock. This ensures that every time your code tries to create an instance of that class, it receives a mock object instead. You can then configure this mock to control the behavior of the returned instance.
Example: Mocking a payment gateway client
Imagine a function that processes an order by creating a PaymentGateway
client and calling its process_payment
method.
payment_service.py
from my_app.payments import PaymentGateway
def process_order(order_data):
gateway = PaymentGateway(api_key='12345')
result = gateway.process_payment(order_data['amount'])
if result.status == 'success':
return 'Payment successful'
return 'Payment failed'
We can mock the PaymentGateway
class to avoid making a real API call.
test_payment_service.py
import unittest
from unittest.mock import patch, MagicMock
from payment_service import process_order
class TestPaymentService(unittest.TestCase):
# Patch the class where it's looked up: in `payment_service` module
@patch('payment_service.PaymentGateway')
def test_successful_payment(self, MockPaymentGateway):
# Configure the mock instance that will be returned when `PaymentGateway()` is called
mock_instance = MockPaymentGateway.return_value
# Configure the return value of a method on the mock instance
mock_instance.process_payment.return_value = MagicMock(status='success')
# Test the function
result = process_order({'amount': 100})
# Assert that the class and method were called with the correct arguments
MockPaymentGateway.assert_called_once_with(api_key='12345')
mock_instance.process_payment.assert_called_once_with(100)
self.assertEqual(result, 'Payment successful')
In this example, @patch('payment_service.PaymentGateway')
replaces the PaymentGateway
class itself with a mock. The mock object returned by the decorator (MockPaymentGateway
) is a callable mock. Its return_value
is what will be returned when your code calls PaymentGateway()
. We then configure the methods on this return_value
mock to simulate the process_payment
call.
Mocking an Instance Method
Sometimes, you don't need to replace an entire class. You might only need to mock a specific method on an object that is passed into your function. This is less intrusive and can be useful for integration-style tests.
Example: Mocking a database method
Consider a function that takes a database client object and calls its get_user
method.
user_service.py
def get_username(db_client, user_id):
user = db_client.get_user(user_id)
return user['name'] if user else 'Unknown'
We can mock just the get_user
method on a passed-in db_client
mock.
test_user_service.py
import unittest
from unittest.mock import MagicMock
from user_service import get_username
class TestUserService(unittest.TestCase):
def test_get_username_found(self):
# Create a mock for the database client instance
mock_db_client = MagicMock()
# Configure only the `get_user` method's return value
mock_db_client.get_user.return_value = {'id': 1, 'name': 'John Doe'}
username = get_username(mock_db_client, 1)
# Assert that the `get_user` method was called correctly
mock_db_client.get_user.assert_called_once_with(1)
self.assertEqual(username, 'John Doe')
Here, we don't use @patch
because we are creating the mock ourselves and passing it directly into the function. This is a common pattern in dependency injection where the function expects its dependencies to be passed in.
pytest-mock
: A Simpler Approach with mocker
While unittest.mock
is powerful, the syntax can be a bit verbose. The pytest-mock
plugin provides a cleaner, pytest
-native way to do the same thing using a fixture named mocker
.
test_payment_service_pytest.py
# `pytest-mock` makes this code cleaner
from my_app.payments import PaymentGateway
from payment_service import process_order
# `mocker` is a fixture provided by pytest-mock
def test_successful_payment_pytest(mocker):
# The mocker.patch function is simpler to use
mock_gateway = mocker.patch('payment_service.PaymentGateway')
# We can directly configure the return value of chained calls
mock_gateway.return_value.process_payment.return_value = mocker.MagicMock(status='success')
result = process_order({'amount': 100})
mock_gateway.assert_called_once_with(api_key='12345')
assert result == 'Payment successful'
The mocker
fixture automatically handles the patching and cleanup, making your tests more concise and readable.
Mocking vs. Stubs vs. Fakes
It's important to understand the subtle differences between these common testing concepts. They are all types of "test doubles"-objects used in place of real ones [4].
- Mock: An object that records interactions and has expectations about how it will be used. You assert on the mock itself (e.g.,
mock.assert_called_once()
). Mocks are used for verifying behavior. - Stub: An object that provides canned responses to method calls. Its purpose is to provide data to the code under test. You don't assert on the stub itself; you assert on the result of the code.
- Fake: A working but simplified implementation of a dependency. A fake database, for example, might store data in a dictionary in memory instead of on disk. Fakes are used for integration tests where you need a lightweight, in-memory version of an external system.
The unittest.mock
library is versatile and can act as a mock, stub, or spy depending on how you use it. For example, by using return_value
you are essentially "stubbing" a return, and by using assert_called
you are "mocking" or verifying a call [4].
This article has covered how to mock classes, instances, and the key terminology. In the final article of this series, we will discuss advanced techniques, best practices, and common pitfalls to help you write truly effective and maintainable tests.
Sources
- Real Python - The Python Mocking Library
- Effective Python Mocking: A Practical Guide
- Python unittest.mock documentation
- Mocks, Stubs, and Fakes: Understanding the Difference
- Pytest-mock plugin documentation
This video provides a practical guide on mocking classes and instances in Python. Mocking a Class with Python's unittest.mock