When writing tests such as unit, integration, or end-to-end, mocking is an essential technique that helps isolate a function’s behavior by replacing its dependencies with test doubles. This makes it easier to test individual modules without interference, leading to more predictable and accurate results.
Pytest is one of the most widely used testing frameworks in Python, and the pytest-mock library makes mocking easier and more flexible. One of many useful features of this library is mocker.spy. This feature helps you monitor function and method calls in a test without modifying the original functions/ methods.
This article explores how mocker.spy works, highlights real-world use cases, and provides practical code examples to help you use it effectively in your tests.
Understanding Mocking in Pytest
Mocking has always been a crucial part of testing, whether it’s unit, integration, or end-to-end testing. Pytest offers powerful tools for mocking, making it easy to replace real objects with mock versions during tests to isolate behavior and ensure reliable results.
Here are a few of many cases where mocking is useful.:
- Testing components in isolation without affecting external dependencies.
- Spying on function calls to verify which arguments they have called with, what values they would return, and their call counts.
- Simulating different behaviors and edge cases without modifying the actual implementation in the code base.
Common Usecases:
- Mocking APIs: Replace network requests with mock responses to avoid real API calls.
- Mocking Database Calls: Prevent tests from interacting with the actual database.
- Spying on Methods: Track how often a method was called and with what arguments.
Pytest offers the pytest-mock plugin to simplify mocking tasks, by providing powerful tools like mocker.spy, mocker.patch, and mocker.stop for more controlled and readable tests.
What is mocker.spy?
Mocking a function allows testing a module in isolation, but verifying that calls to the mocked function behave as expected is important. mocker.spy is a pytest-mock utility that helps monitor function and method calls without changing their behavior. It is useful when you need to:
- Verify how many times a function is called.
- Check the arguments passed to a function.
- Ensure a method is executed in a test without modifying its original logic.
For example, if you have a function that logs messages, you need to spy on it to ensure it’s being called correctly:
Python:
import logging def log_message(): logging.info("This is a test log.") def test_log_spy(mocker): spy = mocker.spy(logging, "info") log_message() spy.assert_called_once_with("This is a test log.")
Output:
How mocker.spy Works
As discussed mocker.spy acts as a wrapper around the target function or method, allowing it to behave normally while capturing call details. To understand it better, here’s an overview of its typical workflow.
Workflow of mocker.spy:
- Attach a spy to a function or method that can be a mocked function or the one you need to monitor.
- Execute the target function that is under the test.
- Record details with spy, such as call count, arguments, and return values.
- Assert with spy to validate the expected behavior.
This makes mocker.spy a powerful tool for verifying function interactions in a non-intrusive way.
Prerequisites for Using mocker.spy
Before using mocker.spy, ensure you have the following installed:
- Python
- Pytest
- Pytest-mock
To install Python you can refer to: https://www.python.org/downloads/.
After installing Python and setting up the system, you can use the below command to install the rest of the libraries.
Command: pip install pytest pytest-mock
Output:
Add pytest-mock as a dependency in your project’s requirements.txt or pyproject.toml. This will help you set up prerequisites quickly next time.
Example of mocker.spy Usage
The following example demonstrates how to use mocker.spy to track function calls while preserving their original behavior.
Example 1: Spying on a Class Method
Python:
def test_class_method_spy(mocker): class Calculator: def add(self, a, b): return a + b calc = Calculator() spy = mocker.spy(calc, "add") result = calc.add(3, 4) assert result == 7 assert spy.call_count == 1 spy.assert_called_with(3, 4)
Output:
Test passes if add() is called once with (3, 4) and returns 7.
Example 2: Spying on a Standalone Function
Python:
def multiply(x, y): return x * y def test_function_spy(mocker): print(__name__) spy = mocker.spy(multiply, "__call__") assert multiply(2, 5) == 10 assert spy.call_count == 1 spy.assert_called_once_with(2, 5)
Output:
Test passes if multiply() is called once with (2, 5) and returns 10.
Example 3: Spying on an Instance Method in a Mocked Class
Python:
def test_instance_method_spy(mocker): class Messenger: def send_message(self, message): return f"Sent: {message}" instance = Messenger() spy = mocker.spy(instance, "send_message") assert instance.send_message("Hello!") == "Sent: Hello!" assert spy.call_count == 1 spy.assert_called_once_with("Hello!")
Output:
Test passes if send_message() is called once with “Hello!”.
When to Use mocker.spy
mocker.spy is useful when you need to track function or method calls without modifying the function’s behavior. It helps to verify the interaction with the function while not interfering with the execution. Here are some key use cases:
1. Verifying Method Calls in Complex Classes
This will ensure that the method is called the correct number of times during the entire execution.
Example: Checking if a specific database query function runs only once with a specific query.
Python:
class Database: def fetch_data(self, query): return f"Data for {query}" def test_database_fetch(mocker): db = Database() spy = mocker.spy(db, "fetch_data") db.fetch_data("SELECT * FROM users") spy.assert_called_once_with("SELECT * FROM users") assert spy.call_count == 1
Output:
2. Tracking API calls
Spy can help test and verify that an API client is calling the right endpoint.
Example: Ensuring an HTTP request function is triggered with the correct URL.
Python:
import requests class APIClient: def get_data(self, url): return f"Function called with {url}" def test_api_client(mocker): client = APIClient() spy = mocker.spy(client, "get_data") client.get_data("https://api.example.com/data") spy.assert_called_once_with("https://api.example.com/data")
Output:
mocker.spy helps verify that an API client is calling the correct endpoint with the expected parameters. This ensures backend integrations behave as intended during testing.
However, to truly ensure reliability, especially in real-world conditions like varying network speeds or browser behavior, tests should go beyond local environments.
BrowserStack makes this possible by running your tests across a wide range of real devices and browsers, helping catch issues that only appear in specific environments. This makes validating API behavior in real user conditions easier before the code reaches production.
3. Checking Function Execution in Event-Driven Systems
Ensure an error-handling function is called on failure.
Example: Verifying a retry mechanism triggers an error log.
Python:
class TaskProcessor: def process(self, task): if task == "fail": self.handle_error("Task failed") def handle_error(self, message): print(f"Error: {message}") def test_retry_mechanism(mocker): processor = TaskProcessor() spy = mocker.spy(processor, "handle_error") processor.process("fail") spy.assert_called_once_with("Task failed")
Output:
4. Testing Logging Mechanism
Verify that logs are generated correctly.
Example: Ensuring an info log message is recorded.
Python:
import logging def log_message(): logging.info("This is a test log.") def test_log_spy(mocker): spy = mocker.spy(logging, "info") log_message() spy.assert_called_once_with("This is a test log.")
Output:
mocker.spy helps validate internal logic, but real-world behavior can vary across browsers and devices. Running Pytest tests on BrowserStack Automate ensures consistent performance in actual environments.
When Not to Use mocker.spy
Use alternative methods in these cases:
Scenario | Better Alternative |
---|---|
You need to completely replace a function | mocker.patch |
The function makes external calls that shouldn’t run | mocker.patch with a return value |
You want to return controlled outputs | mocker.Mock |
Read More: Understanding Pytest BDD
Combining mocker.spy with Other Pytest-Mock Features
mocker.spy is already powerful on its own, but to test a function with all possible use cases, it must be combined with other pytest-mock features, which can enhance your test coverage.
1. Combining mocker.spy with mocker.patch
When you want to track how a function is called but also want to change its behavior, mocker.patch is the feature to use.
Example: Mock an API call while still tracking its usage.
Python
import requests class APIClient: def get_data(self, url): return requests.get(url).json() def test_spy_with_patch(mocker): client = APIClient() # Patch the method to return a controlled response mocker.patch.object(client, "get_data", return_value={"data": "mocked response"}) # Spy on the method spy = mocker.spy(client, "get_data") result = client.get_data("https://api.example.com/data") assert result == {"data": "mocked response"} # Ensures the function returns the patched response spy.assert_called_once_with("https://api.example.com/data") # Ensures the function was called
Output:
2. Combining mocker.spy with mocker.stub
mocker.stub helps you fake a function that mimics real behavior, and to track this mimicked function, you can use mocker.spy. The stub is a mock object that accepts any arguments and is useful to test callbacks.
Example: Create a stub function and spy on its calls.
def send_email(callback_func): return callback_func("test@example.com", "Hello") def test_spy_with_stub(mocker): # Create a stub function stub = mocker.stub(name="send_email_stub") # Spy on the stub function spy = mocker.spy(send_email, "__call__") # Call the stub function response = send_email(stub) stub.assert_called_once_with("test@example.com", "Hello") # Ensures it was called correctly
Output:
3. Combining mocker.spy with mocker.stop
You often want to spy on a function temporarily and stop tracking it to prevent interference with later assertion. mocker.stop provides this capability to remove any spy on a function.
Example: Spy on a logging function but stop tracking it midway.
Python
import logging def log_message(): logging.info("Test log message.") def test_spy_with_unspy(mocker): # Spy on logging.info spy = mocker.spy(logging, "info") log_message() # Call the function spy.assert_called_once_with("Test log message.") # Verify the spy tracked it # Unspy the function mocker.stop(spy) log_message() # Call the function again assert spy.call_count == 1 # Ensures the second call is NOT tracked
Output:
Conclusion
Spying on functions with mocker.spy is an excellent way to track function behavior without modifying implementation. Whether you’re testing class methods, standalone functions, or instance methods, mocker.spy provides a powerful and flexible approach to verifying function calls.
To ensure this verified behavior holds true across real-world conditions, run your automated tests on BrowserStack. It gives you access to thousands of real devices and browsers, helping you catch environment-specific issues early and deliver reliable experiences to every user.