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

LibraryUsage
pytest-mock (mocker fixture)Preferred for new projects and test migrations
monkeypatchAcceptable 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.pytest_parser.py).
  • Fixtures in conftest.py when 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