Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Apply Python testing best practices with pytest, fixtures, mocking, and coverage strategies.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/details.md
1# python-testing-patterns — detailed patterns and worked examples23## Fundamental Patterns45### Pattern 1: Basic pytest Tests67```python8# test_calculator.py9import pytest1011class Calculator:12"""Simple calculator for testing."""1314def add(self, a: float, b: float) -> float:15return a + b1617def subtract(self, a: float, b: float) -> float:18return a - b1920def multiply(self, a: float, b: float) -> float:21return a * b2223def divide(self, a: float, b: float) -> float:24if b == 0:25raise ValueError("Cannot divide by zero")26return a / b272829def test_addition():30"""Test addition."""31calc = Calculator()32assert calc.add(2, 3) == 533assert calc.add(-1, 1) == 034assert calc.add(0, 0) == 0353637def test_subtraction():38"""Test subtraction."""39calc = Calculator()40assert calc.subtract(5, 3) == 241assert calc.subtract(0, 5) == -5424344def test_multiplication():45"""Test multiplication."""46calc = Calculator()47assert calc.multiply(3, 4) == 1248assert calc.multiply(0, 5) == 0495051def test_division():52"""Test division."""53calc = Calculator()54assert calc.divide(6, 3) == 255assert calc.divide(5, 2) == 2.5565758def test_division_by_zero():59"""Test division by zero raises error."""60calc = Calculator()61with pytest.raises(ValueError, match="Cannot divide by zero"):62calc.divide(5, 0)63```6465### Pattern 2: Fixtures for Setup and Teardown6667```python68# test_database.py69import pytest70from typing import Generator7172class Database:73"""Simple database class."""7475def __init__(self, connection_string: str):76self.connection_string = connection_string77self.connected = False7879def connect(self):80"""Connect to database."""81self.connected = True8283def disconnect(self):84"""Disconnect from database."""85self.connected = False8687def query(self, sql: str) -> list:88"""Execute query."""89if not self.connected:90raise RuntimeError("Not connected")91return [{"id": 1, "name": "Test"}]929394@pytest.fixture95def db() -> Generator[Database, None, None]:96"""Fixture that provides connected database."""97# Setup98database = Database("sqlite:///:memory:")99database.connect()100101# Provide to test102yield database103104# Teardown105database.disconnect()106107108def test_database_query(db):109"""Test database query with fixture."""110results = db.query("SELECT * FROM users")111assert len(results) == 1112assert results[0]["name"] == "Test"113114115@pytest.fixture(scope="session")116def app_config():117"""Session-scoped fixture - created once per test session."""118return {119"database_url": "postgresql://localhost/test",120"api_key": "test-key",121"debug": True122}123124125@pytest.fixture(scope="module")126def api_client(app_config):127"""Module-scoped fixture - created once per test module."""128# Setup expensive resource129client = {"config": app_config, "session": "active"}130yield client131# Cleanup132client["session"] = "closed"133134135def test_api_client(api_client):136"""Test using api client fixture."""137assert api_client["session"] == "active"138assert api_client["config"]["debug"] is True139```140141### Pattern 3: Parameterized Tests142143```python144# test_validation.py145import pytest146147def is_valid_email(email: str) -> bool:148"""Check if email is valid."""149return "@" in email and "." in email.split("@")[1]150151152@pytest.mark.parametrize("email,expected", [153("[email protected]", True),154("[email protected]", True),155("invalid.email", False),156("@example.com", False),157("user@domain", False),158("", False),159])160def test_email_validation(email, expected):161"""Test email validation with various inputs."""162assert is_valid_email(email) == expected163164165@pytest.mark.parametrize("a,b,expected", [166(2, 3, 5),167(0, 0, 0),168(-1, 1, 0),169(100, 200, 300),170(-5, -5, -10),171])172def test_addition_parameterized(a, b, expected):173"""Test addition with multiple parameter sets."""174from test_calculator import Calculator175calc = Calculator()176assert calc.add(a, b) == expected177178179# Using pytest.param for special cases180@pytest.mark.parametrize("value,expected", [181pytest.param(1, True, id="positive"),182pytest.param(0, False, id="zero"),183pytest.param(-1, False, id="negative"),184])185def test_is_positive(value, expected):186"""Test with custom test IDs."""187assert (value > 0) == expected188```189190### Pattern 4: Mocking with unittest.mock191192```python193# test_api_client.py194import pytest195from unittest.mock import Mock, patch, MagicMock196import requests197198class APIClient:199"""Simple API client."""200201def __init__(self, base_url: str):202self.base_url = base_url203204def get_user(self, user_id: int) -> dict:205"""Fetch user from API."""206response = requests.get(f"{self.base_url}/users/{user_id}")207response.raise_for_status()208return response.json()209210def create_user(self, data: dict) -> dict:211"""Create new user."""212response = requests.post(f"{self.base_url}/users", json=data)213response.raise_for_status()214return response.json()215216217def test_get_user_success():218"""Test successful API call with mock."""219client = APIClient("https://api.example.com")220221mock_response = Mock()222mock_response.json.return_value = {"id": 1, "name": "John Doe"}223mock_response.raise_for_status.return_value = None224225with patch("requests.get", return_value=mock_response) as mock_get:226user = client.get_user(1)227228assert user["id"] == 1229assert user["name"] == "John Doe"230mock_get.assert_called_once_with("https://api.example.com/users/1")231232233def test_get_user_not_found():234"""Test API call with 404 error."""235client = APIClient("https://api.example.com")236237mock_response = Mock()238mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")239240with patch("requests.get", return_value=mock_response):241with pytest.raises(requests.HTTPError):242client.get_user(999)243244245@patch("requests.post")246def test_create_user(mock_post):247"""Test user creation with decorator syntax."""248client = APIClient("https://api.example.com")249250mock_post.return_value.json.return_value = {"id": 2, "name": "Jane Doe"}251mock_post.return_value.raise_for_status.return_value = None252253user_data = {"name": "Jane Doe", "email": "[email protected]"}254result = client.create_user(user_data)255256assert result["id"] == 2257mock_post.assert_called_once()258call_args = mock_post.call_args259assert call_args.kwargs["json"] == user_data260```261262### Pattern 5: Testing Exceptions263264```python265# test_exceptions.py266import pytest267268def divide(a: float, b: float) -> float:269"""Divide a by b."""270if b == 0:271raise ZeroDivisionError("Division by zero")272if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):273raise TypeError("Arguments must be numbers")274return a / b275276277def test_zero_division():278"""Test exception is raised for division by zero."""279with pytest.raises(ZeroDivisionError):280divide(10, 0)281282283def test_zero_division_with_message():284"""Test exception message."""285with pytest.raises(ZeroDivisionError, match="Division by zero"):286divide(5, 0)287288289def test_type_error():290"""Test type error exception."""291with pytest.raises(TypeError, match="must be numbers"):292divide("10", 5)293294295def test_exception_info():296"""Test accessing exception info."""297with pytest.raises(ValueError) as exc_info:298int("not a number")299300assert "invalid literal" in str(exc_info.value)301```302303For advanced patterns including async testing, monkeypatching, temporary files, conftest setup, property-based testing, database testing, CI/CD integration, and configuration files, see [references/advanced-patterns.md](references/advanced-patterns.md)304305## Test Design Principles306307### One Behavior Per Test308309Each test should verify exactly one behavior. This makes failures easy to diagnose and tests easy to maintain.310311```python312# BAD - testing multiple behaviors313def test_user_service():314user = service.create_user(data)315assert user.id is not None316assert user.email == data["email"]317updated = service.update_user(user.id, {"name": "New"})318assert updated.name == "New"319320# GOOD - focused tests321def test_create_user_assigns_id():322user = service.create_user(data)323assert user.id is not None324325def test_create_user_stores_email():326user = service.create_user(data)327assert user.email == data["email"]328329def test_update_user_changes_name():330user = service.create_user(data)331updated = service.update_user(user.id, {"name": "New"})332assert updated.name == "New"333```334335### Test Error Paths336337Always test failure cases, not just happy paths.338339```python340def test_get_user_raises_not_found():341with pytest.raises(UserNotFoundError) as exc_info:342service.get_user("nonexistent-id")343344assert "nonexistent-id" in str(exc_info.value)345346def test_create_user_rejects_invalid_email():347with pytest.raises(ValueError, match="Invalid email format"):348service.create_user({"email": "not-an-email"})349```350