Python Test Instructions
June 9, 2026 · View on GitHub
Conventions for Python test code. All conventions from python-script.instructions.md apply.
Test Framework
Use pytest with BDD-style naming. Structure each test with Arrange/Act/Assert (AAA) sections separated by blank lines and comments.
Mocking Libraries
| Library | Usage |
|---|---|
pytest-mock (mocker fixture) | Preferred for new projects and test migrations |
| monkeypatch | Acceptable for simple attribute/environment patching |
| unittest.mock (direct import) | Existing projects only; migrate to mocker when editing |
When to Use mocker vs monkeypatch
mocker.patch()— replacing functions, methods, classes, or module attributes with controlled return values or side effects; verifying call counts and arguments.monkeypatch.setattr()— simple attribute overrides (constants, config values, environment variables) where return tracking is not needed.- Direct
MagicMock()import — acceptable for constructing pure test data stubs (mock objects used as constructor arguments, not as spy/assert targets).
Test Naming
Test method format: test_given_context_when_action_then_expected
test_given_valid_request_when_process_data_then_returns_parsed_response
test_given_empty_input_when_process_data_then_raises_value_error
test_given_missing_config_when_initialize_then_exits_with_error
Prefer one assertion per test. Related assertions validating the same behavior are acceptable. Do not verify logger mocks.
Use @pytest.mark.parametrize for data-driven tests with multiple input/output combinations.
Test Organization
- File naming mirrors module under test with
test_prefix (for example,parser.py→test_parser.py). - Fixtures in
conftest.pywhen shared across multiple test files. - Class-based grouping optional; use when tests share setup logic.
- Group test methods by behavior, alphabetically within groups.
- Common mock setup in fixtures or class-level setup; specific setup in individual tests.
pytest-mock Patterns
The mocker fixture from pytest-mock replaces direct unittest.mock usage. These patterns show each migration.
mocker.patch() replacing @patch decorator
# Before — unittest.mock
from unittest.mock import patch
@patch("myapp.service.fetch_data")
def test_process_uses_fetched_data(mock_fetch):
mock_fetch.return_value = {"key": "value"}
result = process()
assert result == "value"
# After — pytest-mock
def test_process_uses_fetched_data(mocker):
mock_fetch = mocker.patch("myapp.service.fetch_data", return_value={"key": "value"})
result = process()
assert result == "value"
mock_fetch.assert_called_once()
mocker.patch() replacing with patch() context manager
# Before — unittest.mock
from unittest.mock import patch
def test_service_calls_endpoint():
with patch("myapp.client.post") as mock_post:
mock_post.return_value.status_code = 200
response = send_request()
assert response.status_code == 200
# After — pytest-mock
def test_service_calls_endpoint(mocker):
mock_post = mocker.patch("myapp.client.post")
mock_post.return_value.status_code = 200
response = send_request()
assert response.status_code == 200
mocker.patch.dict() replacing @patch.dict
# Before — unittest.mock
from unittest.mock import patch
@patch.dict("os.environ", {"API_KEY": "test-key"})
def test_config_reads_env():
config = load_config()
assert config.api_key == "test-key"
# After — pytest-mock
def test_config_reads_env(mocker):
mocker.patch.dict("os.environ", {"API_KEY": "test-key"})
config = load_config()
assert config.api_key == "test-key"
mocker.patch.object() replacing patch.object()
# Before — unittest.mock
from unittest.mock import patch
from myapp.service import DataService
@patch.object(DataService, "connect")
def test_service_connects(mock_connect):
mock_connect.return_value = True
svc = DataService()
assert svc.connect() is True
# After — pytest-mock
from myapp.service import DataService
def test_service_connects(mocker):
mock_connect = mocker.patch.object(DataService, "connect", return_value=True)
svc = DataService()
assert svc.connect() is True
mock_connect.assert_called_once()
mocker.MagicMock() and mocker.AsyncMock() for spy targets
Use mocker.MagicMock() and mocker.AsyncMock() when constructing mock objects that serve as spy targets for call assertion:
def test_handler_delegates_to_processor(mocker):
mock_processor = mocker.MagicMock()
handler = RequestHandler(processor=mock_processor)
handler.handle({"id": 1})
mock_processor.process.assert_called_once_with({"id": 1})
async def test_async_handler_awaits_processor(mocker):
mock_processor = mocker.AsyncMock()
handler = AsyncRequestHandler(processor=mock_processor)
await handler.handle({"id": 1})
mock_processor.process.assert_awaited_once_with({"id": 1})
Direct MagicMock() import for test data stubs
Direct MagicMock() import stays as-is when constructing pure test data stubs that are not spy/assert targets:
from unittest.mock import MagicMock
def test_formatter_accepts_any_writer():
# Arrange
stub_writer = MagicMock()
stub_writer.encoding = "utf-8"
formatter = OutputFormatter(writer=stub_writer)
# Act
result = formatter.format("hello")
# Assert
assert result == "hello"
Complete Example
A full test class using the mocker fixture with AAA structure:
import pytest # noqa: F811
from myapp.processor import DataProcessor
from myapp.service import DataService
class TestDataProcessor:
@pytest.fixture()
def mock_service(self, mocker):
return mocker.patch.object(DataService, "fetch", return_value={"status": "ok", "value": 42})
@pytest.fixture()
def processor(self):
return DataProcessor(service=DataService())
def test_given_valid_response_when_process_then_returns_value(self, processor, mock_service):
# Act
result = processor.process()
# Assert
assert result == 42
mock_service.assert_called_once()
def test_given_error_response_when_process_then_raises(self, processor, mocker):
# Arrange
mocker.patch.object(DataService, "fetch", side_effect=ConnectionError("timeout"))
# Act & Assert
with pytest.raises(ConnectionError, match="timeout"):
processor.process()
@pytest.mark.parametrize(
("status", "expected"),
[
("ok", 42),
("pending", 0),
],
)
def test_given_status_when_process_then_returns_expected(self, mocker, status, expected):
# Arrange
mocker.patch.object(DataService, "fetch", return_value={"status": status, "value": expected})
processor = DataProcessor(service=DataService())
# Act
result = processor.process()
# Assert
assert result == expected