Pytest fixtures can be more than just setup and teardown; they’re a powerful way to manage dependencies and isolate code for testing, especially when combined with mocking.
Let’s see a fixture that uses mocking to simulate an external API call. Imagine we have a function that fetches data from a remote service:
# my_module.py
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status() # Raise an exception for bad status codes
return response.json()
Now, we want to test get_user_data without actually making a network request. We can use a fixture with unittest.mock.patch to intercept the requests.get call:
# test_my_module.py
import pytest
import requests
from unittest.mock import MagicMock
from my_module import get_user_data
@pytest.fixture
def mock_api_response():
"""Mocks the requests.get call to return a predefined JSON response."""
with patch('my_module.requests.get') as mock_get:
# Create a mock response object
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"id": 123, "name": "Alice"}
mock_response.raise_for_status.return_value = None # Simulate success
# Configure the mock_get to return our mock_response
mock_get.return_value = mock_response
yield mock_get # Yield the mock object itself for potential further inspection
def test_get_user_data_success(mock_api_response):
user_id = 123
user_data = get_user_data(user_id)
# Assert that requests.get was called with the correct URL
mock_api_response.assert_called_once_with(f"https://api.example.com/users/{user_id}")
# Assert that the function returned the mocked data
assert user_data == {"id": 123, "name": "Alice"}
def test_get_user_data_api_error(mock_api_response):
user_id = 404
# Configure the mock to simulate an API error (e.g., 404 Not Found)
mock_api_response.side_effect = requests.exceptions.HTTPError("404 Client Error: Not Found for url: https://api.example.com/users/404")
with pytest.raises(requests.exceptions.HTTPError):
get_user_data(user_id)
# Ensure the original get call was still made
mock_api_response.assert_called_once_with(f"https://api.example.com/users/{user_id}")
This fixture, mock_api_response, uses unittest.mock.patch to replace requests.get within the my_module namespace only for the duration of the test. The with statement ensures the patch is automatically cleaned up. We then configure the mock object to return a MagicMock that mimics a successful requests.Response object, complete with a status_code, a json() method returning our desired data, and a raise_for_status() method that does nothing (simulating a 2xx status).
The tests then use this fixture. test_get_user_data_success verifies that get_user_data correctly calls the mocked requests.get with the expected URL and processes the mocked JSON response. test_get_user_data_api_error demonstrates how to configure the mock to raise an exception, simulating an API error, and then uses pytest.raises to assert that our function correctly propagates this error.
The most surprising thing about this setup is how unittest.mock.patch works by replacing the object in the namespace where it’s looked up, not where it’s defined. This is why we patch 'my_module.requests.get' and not 'requests.get'. If my_module imports requests as import requests, then my_module looks for get on its own requests object. Patching 'requests.get' would only affect code that directly imports and uses requests.get.
When you have multiple fixtures that need to mock different parts of the same external dependency, you can chain them. For example, if you also needed to mock requests.post, you could create another fixture that also patches requests.get and requests.post, or have one fixture patch requests itself and then configure mock_requests.get and mock_requests.post within that single fixture.
The next concept you’ll likely encounter is using fixtures with parametrized tests to run the same test logic against many different mocked responses or input scenarios.