Pytest Quick Start Guide
Let's get you up to speed on Pytest, mocking, fixtures, patching, and monkey patching so you can start writing tests quickly!
Pytest Fundamentals
Pytest is a powerful and easy-to-use testing framework for Python.
- Installation:
pip install pytest - Running Tests: Navigate to your project directory in the terminal and run
pytest. Pytest automatically discovers test files (files starting withtest_or ending with_test.py) and test functions/methods (functions starting withtest_). - Assertions: Pytest uses standard Python
assertstatements.Pythondef test_addition(): assert 1 + 1 == 2 def test_list_contains(): my_list = [1, 2, 3] assert 2 in my_list - Test Classes: You can group related tests into classes.
Python
class TestCalculator: def test_add(self): assert 2 + 3 == 5 def test_subtract(self): assert 5 - 2 == 3
Fixtures
Fixtures are functions that Pytest runs before (and sometimes after) your tests. They are used to set up a known working environment for your tests, such as creating a temporary database, setting up a test client, or providing test data.
- Definition: Decorate a function with
@pytest.fixture(). - Usage: Pass the fixture function's name as an argument to your test function. Pytest will automatically discover and run the fixture.
- Scope: Fixtures can have different scopes:
function(default): Run once per test function.class: Run once per test class.module: Run once per module.session: Run once per Pytest session.
Example:
import pytest
@pytest.fixture
def sample_data():
"""A fixture that provides a dictionary of sample data."""
return {"name": "Alice", "age": 30}
def test_user_name(sample_data):
assert sample_data["name"] == "Alice"
def test_user_age(sample_data):
assert sample_data["age"] == 30
Mocking & Patching
Mocking is the act of replacing parts of your system under test with mock objects that simulate the behavior of real objects. This is crucial for isolating the code you're testing and preventing external dependencies (like databases, network calls, or complex calculations) from affecting your tests.
unittest.mock (built-in in Python 3.3+) is the standard library for mocking. Pytest integrates well with it.
-
unittest.mock.Mock: A flexible class that allows you to create mock objects. You can define their return values, side effects, and track how they are called.Pythonfrom unittest.mock import Mock def test_mock_return_value(): my_mock = Mock(return_value="mocked_result") assert my_mock() == "mocked_result" def test_mock_calls(): my_mock = Mock() my_mock.do_something("arg1", keyword_arg="value") my_mock.do_something.assert_called_with("arg1", keyword_arg="value") my_mock.do_something.assert_called_once() -
unittest.mock.patch: The most common way to replace objects with mocks. It's a context manager or a decorator that temporarily replaces an object during the execution of your test.Key Principle for Patching: You need to patch where an object is looked up, not necessarily where it's defined.
Example (assuming
my_module.pyhasfetch_data_from_api()):Python# my_module.py import requests def fetch_data_from_api(url): response = requests.get(url) response.raise_for_status() return response.json() # test_my_module.py from unittest.mock import patch from my_module import fetch_data_from_api def test_fetch_data_success(): with patch('my_module.requests.get') as mock_get: mock_response = Mock() mock_response.json.return_value = {"key": "value"} mock_get.return_value = mock_response result = fetch_data_from_api("http://example.com") assert result == {"key": "value"} mock_get.assert_called_once_with("http://example.com") # Using @patch as a decorator (more common for functions) @patch('my_module.requests.get') def test_fetch_data_success_decorator(mock_get): # mock_get is passed as an argument mock_response = Mock() mock_response.json.return_value = {"key": "value"} mock_get.return_value = mock_response result = fetch_data_from_api("http://example.com") assert result == {"key": "value"} mock_get.assert_called_once_with("http://example.com") -
pytest-mock(Highly Recommended for Pytest Users): This plugin provides a Pytest fixture namedmockerwhich simplifies patching. It's essentially a wrapper aroundunittest.mockbut with Pytest's fixture system, meaning the patches are automatically cleaned up after each test.- Installation:
pip install pytest-mock - Usage:
Python
# test_my_module.py from my_module import fetch_data_from_api def test_fetch_data_with_mocker(mocker): # 'mocker' is the fixture mock_get = mocker.patch('my_module.requests.get') mock_response = mocker.Mock() # Use mocker.Mock() or unittest.mock.Mock() mock_response.json.return_value = {"key": "value"} mock_get.return_value = mock_response result = fetch_data_from_api("http://example.com") assert result == {"key": "value"} mock_get.assert_called_once_with("http://example.com")mocker.patchis cleaner and integrates perfectly with Pytest's fixture system for automatic cleanup.
- Installation:
Monkey Patching
Monkey patching refers to modifying or extending a program at runtime by changing or adding to an existing class, module, or function. While powerful, it can also lead to less maintainable and harder-to-debug code if overused.
In the context of testing, patching (as discussed above) is a form of monkey patching, specifically for testing purposes. unittest.mock.patch and pytest-mock.mocker.patch handle the temporary modification and restoration for you.
-
When to use (carefully): When you need to temporarily change the behavior of a function or method that is difficult to mock directly (e.g., a function in a third-party library that you can't modify, or a global variable).
-
Pytest's
monkeypatchfixture: Pytest provides a built-inmonkeypatchfixture for this purpose, offering a safe and controlled way to perform monkey patching. It automatically undoes the changes after the test.monkeypatch.setattr(obj, name, value): Sets an attribute on an object.monkeypatch.delattr(obj, name): Deletes an attribute.monkeypatch.setenv(name, value): Sets an environment variable.monkeypatch.delenv(name): Deletes an environment variable.monkeypatch.syspath_prepend(path): Prepends a path tosys.path.
Example:
Python# my_settings.py DEFAULT_TIMEOUT = 5 # my_app.py import my_settings def get_timeout(): return my_settings.DEFAULT_TIMEOUT # test_my_app.py import pytest from my_app import get_timeout def test_get_timeout_modified(monkeypatch): # Temporarily change DEFAULT_TIMEOUT for this test monkeypatch.setattr('my_settings.DEFAULT_TIMEOUT', 10) assert get_timeout() == 10 def test_get_timeout_original(): # After the previous test, the original value is restored assert get_timeout() == 5
Quick Guide to Writing Tests for Your Classes
- Identify Units: Determine the smallest independent units of code you want to test (usually public methods of your classes).
- Isolate Dependencies: For each method, identify its external dependencies (other classes, functions, network calls, database calls, file system operations).
- Use Fixtures for Setup: If multiple tests need the same setup (e.g., an instance of your class with specific initial state), create a Pytest fixture.
- Mock External Dependencies: Use
mocker.patch(frompytest-mock) orunittest.mock.patchto replace external dependencies with mocks. Define the behavior of these mocks (return values, side effects). - Assert Outcomes: Use
assertstatements to verify:- The return value of the method under test.
- Side effects (e.g., if a mock method was called with specific arguments).
- State changes in your object.
- Exceptions raised.
Example Class and Test Structure:
# my_service.py
import requests
class DataService:
def __init__(self, base_url):
self.base_url = base_url
def fetch_user_data(self, user_id):
try:
response = requests.get(f"{self.base_url}/users/{user_id}")
response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching data: {e}")
return None
def save_data(self, data):
# Imagine this saves to a database
print(f"Saving data: {data}")
return True
# test_my_service.py
import pytest
from my_service import DataService
from unittest.mock import Mock # Can also use mocker.Mock() with pytest-mock
@pytest.fixture
def data_service_instance():
"""Fixture to provide a DataService instance."""
return DataService("http://api.example.com")
def test_fetch_user_data_success(data_service_instance, mocker):
# 1. Mock the external dependency: requests.get
mock_get = mocker.patch('requests.get')
# 2. Configure the mock's behavior (return value of the response)
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"id": 1, "name": "Test User"}
mock_get.return_value = mock_response
# 3. Call the method under test
result = data_service_instance.fetch_user_data(1)
# 4. Assert the outcome
assert result == {"id": 1, "name": "Test User"}
mock_get.assert_called_once_with("http://api.example.com/users/1")
def test_fetch_user_data_network_error(data_service_instance, mocker):
# 1. Mock the external dependency to raise an exception
mock_get = mocker.patch('requests.get')
mock_get.side_effect = requests.exceptions.ConnectionError("Connection refused")
# 2. Call the method under test
result = data_service_instance.fetch_user_data(2)
# 3. Assert the outcome (e.g., None is returned in this case)
assert result is None
mock_get.assert_called_once_with("http://api.example.com/users/2")
def test_save_data_prints_message(data_service_instance, capsys):
# Using capsys fixture to capture stdout/stderr
data_service_instance.save_data({"id": 3, "data": "abc"})
captured = capsys.readouterr()
assert "Saving data: {'id': 3, 'data': 'abc'}" in captured.out
This should give you a solid foundation to start writing effective tests for your Python classes using Pytest and mocking techniques! Good luck!
Comments