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.
SKILL.md
1---2name: python-testing-patterns3description: Implement comprehensive testing strategies with pytest, fixtures, mocking, and test-driven development. Use when writing Python tests, setting up test suites, or implementing testing best practices.4---56# Python Testing Patterns78Comprehensive guide to implementing robust testing strategies in Python using pytest, fixtures, mocking, parameterization, and test-driven development practices.910## When to Use This Skill1112- Writing unit tests for Python code13- Setting up test suites and test infrastructure14- Implementing test-driven development (TDD)15- Creating integration tests for APIs and services16- Mocking external dependencies and services17- Testing async code and concurrent operations18- Setting up continuous testing in CI/CD19- Implementing property-based testing20- Testing database operations21- Debugging failing tests2223## Core Concepts2425### 1. Test Types2627- **Unit Tests**: Test individual functions/classes in isolation28- **Integration Tests**: Test interaction between components29- **Functional Tests**: Test complete features end-to-end30- **Performance Tests**: Measure speed and resource usage3132### 2. Test Structure (AAA Pattern)3334- **Arrange**: Set up test data and preconditions35- **Act**: Execute the code under test36- **Assert**: Verify the results3738### 3. Test Coverage3940- Measure what code is exercised by tests41- Identify untested code paths42- Aim for meaningful coverage, not just high percentages4344### 4. Test Isolation4546- Tests should be independent47- No shared state between tests48- Each test should clean up after itself4950## Quick Start5152```python53# test_example.py54def add(a, b):55return a + b5657def test_add():58"""Basic test example."""59result = add(2, 3)60assert result == 56162def test_add_negative():63"""Test with negative numbers."""64assert add(-1, 1) == 06566# Run with: pytest test_example.py67```6869## Fundamental Patterns7071### Pattern 1: Basic pytest Tests7273```python74# test_calculator.py75import pytest7677class Calculator:78"""Simple calculator for testing."""7980def add(self, a: float, b: float) -> float:81return a + b8283def subtract(self, a: float, b: float) -> float:84return a - b8586def multiply(self, a: float, b: float) -> float:87return a * b8889def divide(self, a: float, b: float) -> float:90if b == 0:91raise ValueError("Cannot divide by zero")92return a / b939495def test_addition():96"""Test addition."""97calc = Calculator()98assert calc.add(2, 3) == 599assert calc.add(-1, 1) == 0100assert calc.add(0, 0) == 0101102103def test_subtraction():104"""Test subtraction."""105calc = Calculator()106assert calc.subtract(5, 3) == 2107assert calc.subtract(0, 5) == -5108109110def test_multiplication():111"""Test multiplication."""112calc = Calculator()113assert calc.multiply(3, 4) == 12114assert calc.multiply(0, 5) == 0115116117def test_division():118"""Test division."""119calc = Calculator()120assert calc.divide(6, 3) == 2121assert calc.divide(5, 2) == 2.5122123124def test_division_by_zero():125"""Test division by zero raises error."""126calc = Calculator()127with pytest.raises(ValueError, match="Cannot divide by zero"):128calc.divide(5, 0)129```130131### Pattern 2: Fixtures for Setup and Teardown132133```python134# test_database.py135import pytest136from typing import Generator137138class Database:139"""Simple database class."""140141def __init__(self, connection_string: str):142self.connection_string = connection_string143self.connected = False144145def connect(self):146"""Connect to database."""147self.connected = True148149def disconnect(self):150"""Disconnect from database."""151self.connected = False152153def query(self, sql: str) -> list:154"""Execute query."""155if not self.connected:156raise RuntimeError("Not connected")157return [{"id": 1, "name": "Test"}]158159160@pytest.fixture161def db() -> Generator[Database, None, None]:162"""Fixture that provides connected database."""163# Setup164database = Database("sqlite:///:memory:")165database.connect()166167# Provide to test168yield database169170# Teardown171database.disconnect()172173174def test_database_query(db):175"""Test database query with fixture."""176results = db.query("SELECT * FROM users")177assert len(results) == 1178assert results[0]["name"] == "Test"179180181@pytest.fixture(scope="session")182def app_config():183"""Session-scoped fixture - created once per test session."""184return {185"database_url": "postgresql://localhost/test",186"api_key": "test-key",187"debug": True188}189190191@pytest.fixture(scope="module")192def api_client(app_config):193"""Module-scoped fixture - created once per test module."""194# Setup expensive resource195client = {"config": app_config, "session": "active"}196yield client197# Cleanup198client["session"] = "closed"199200201def test_api_client(api_client):202"""Test using api client fixture."""203assert api_client["session"] == "active"204assert api_client["config"]["debug"] is True205```206207### Pattern 3: Parameterized Tests208209```python210# test_validation.py211import pytest212213def is_valid_email(email: str) -> bool:214"""Check if email is valid."""215return "@" in email and "." in email.split("@")[1]216217218@pytest.mark.parametrize("email,expected", [219("[email protected]", True),220("[email protected]", True),221("invalid.email", False),222("@example.com", False),223("user@domain", False),224("", False),225])226def test_email_validation(email, expected):227"""Test email validation with various inputs."""228assert is_valid_email(email) == expected229230231@pytest.mark.parametrize("a,b,expected", [232(2, 3, 5),233(0, 0, 0),234(-1, 1, 0),235(100, 200, 300),236(-5, -5, -10),237])238def test_addition_parameterized(a, b, expected):239"""Test addition with multiple parameter sets."""240from test_calculator import Calculator241calc = Calculator()242assert calc.add(a, b) == expected243244245# Using pytest.param for special cases246@pytest.mark.parametrize("value,expected", [247pytest.param(1, True, id="positive"),248pytest.param(0, False, id="zero"),249pytest.param(-1, False, id="negative"),250])251def test_is_positive(value, expected):252"""Test with custom test IDs."""253assert (value > 0) == expected254```255256### Pattern 4: Mocking with unittest.mock257258```python259# test_api_client.py260import pytest261from unittest.mock import Mock, patch, MagicMock262import requests263264class APIClient:265"""Simple API client."""266267def __init__(self, base_url: str):268self.base_url = base_url269270def get_user(self, user_id: int) -> dict:271"""Fetch user from API."""272response = requests.get(f"{self.base_url}/users/{user_id}")273response.raise_for_status()274return response.json()275276def create_user(self, data: dict) -> dict:277"""Create new user."""278response = requests.post(f"{self.base_url}/users", json=data)279response.raise_for_status()280return response.json()281282283def test_get_user_success():284"""Test successful API call with mock."""285client = APIClient("https://api.example.com")286287mock_response = Mock()288mock_response.json.return_value = {"id": 1, "name": "John Doe"}289mock_response.raise_for_status.return_value = None290291with patch("requests.get", return_value=mock_response) as mock_get:292user = client.get_user(1)293294assert user["id"] == 1295assert user["name"] == "John Doe"296mock_get.assert_called_once_with("https://api.example.com/users/1")297298299def test_get_user_not_found():300"""Test API call with 404 error."""301client = APIClient("https://api.example.com")302303mock_response = Mock()304mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")305306with patch("requests.get", return_value=mock_response):307with pytest.raises(requests.HTTPError):308client.get_user(999)309310311@patch("requests.post")312def test_create_user(mock_post):313"""Test user creation with decorator syntax."""314client = APIClient("https://api.example.com")315316mock_post.return_value.json.return_value = {"id": 2, "name": "Jane Doe"}317mock_post.return_value.raise_for_status.return_value = None318319user_data = {"name": "Jane Doe", "email": "[email protected]"}320result = client.create_user(user_data)321322assert result["id"] == 2323mock_post.assert_called_once()324call_args = mock_post.call_args325assert call_args.kwargs["json"] == user_data326```327328### Pattern 5: Testing Exceptions329330```python331# test_exceptions.py332import pytest333334def divide(a: float, b: float) -> float:335"""Divide a by b."""336if b == 0:337raise ZeroDivisionError("Division by zero")338if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):339raise TypeError("Arguments must be numbers")340return a / b341342343def test_zero_division():344"""Test exception is raised for division by zero."""345with pytest.raises(ZeroDivisionError):346divide(10, 0)347348349def test_zero_division_with_message():350"""Test exception message."""351with pytest.raises(ZeroDivisionError, match="Division by zero"):352divide(5, 0)353354355def test_type_error():356"""Test type error exception."""357with pytest.raises(TypeError, match="must be numbers"):358divide("10", 5)359360361def test_exception_info():362"""Test accessing exception info."""363with pytest.raises(ValueError) as exc_info:364int("not a number")365366assert "invalid literal" in str(exc_info.value)367```368369For 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)370371## Test Design Principles372373### One Behavior Per Test374375Each test should verify exactly one behavior. This makes failures easy to diagnose and tests easy to maintain.376377```python378# BAD - testing multiple behaviors379def test_user_service():380user = service.create_user(data)381assert user.id is not None382assert user.email == data["email"]383updated = service.update_user(user.id, {"name": "New"})384assert updated.name == "New"385386# GOOD - focused tests387def test_create_user_assigns_id():388user = service.create_user(data)389assert user.id is not None390391def test_create_user_stores_email():392user = service.create_user(data)393assert user.email == data["email"]394395def test_update_user_changes_name():396user = service.create_user(data)397updated = service.update_user(user.id, {"name": "New"})398assert updated.name == "New"399```400401### Test Error Paths402403Always test failure cases, not just happy paths.404405```python406def test_get_user_raises_not_found():407with pytest.raises(UserNotFoundError) as exc_info:408service.get_user("nonexistent-id")409410assert "nonexistent-id" in str(exc_info.value)411412def test_create_user_rejects_invalid_email():413with pytest.raises(ValueError, match="Invalid email format"):414service.create_user({"email": "not-an-email"})415```416417## Testing Best Practices418419### Test Organization420421```python422# tests/423# __init__.py424# conftest.py # Shared fixtures425# test_unit/ # Unit tests426# test_models.py427# test_utils.py428# test_integration/ # Integration tests429# test_api.py430# test_database.py431# test_e2e/ # End-to-end tests432# test_workflows.py433```434435### Test Naming Convention436437A common pattern: `test_<unit>_<scenario>_<expected_outcome>`. Adapt to your team's preferences.438439```python440# Pattern: test_<unit>_<scenario>_<expected>441def test_create_user_with_valid_data_returns_user():442...443444def test_create_user_with_duplicate_email_raises_conflict():445...446447def test_get_user_with_unknown_id_returns_none():448...449450# Good test names - clear and descriptive451def test_user_creation_with_valid_data():452"""Clear name describes what is being tested."""453pass454455def test_login_fails_with_invalid_password():456"""Name describes expected behavior."""457pass458459def test_api_returns_404_for_missing_resource():460"""Specific about inputs and expected outcomes."""461pass462463# Bad test names - avoid these464def test_1(): # Not descriptive465pass466467def test_user(): # Too vague468pass469470def test_function(): # Doesn't explain what's tested471pass472```473474### Testing Retry Behavior475476Verify that retry logic works correctly using mock side effects.477478```python479from unittest.mock import Mock480481def test_retries_on_transient_error():482"""Test that service retries on transient failures."""483client = Mock()484# Fail twice, then succeed485client.request.side_effect = [486ConnectionError("Failed"),487ConnectionError("Failed"),488{"status": "ok"},489]490491service = ServiceWithRetry(client, max_retries=3)492result = service.fetch()493494assert result == {"status": "ok"}495assert client.request.call_count == 3496497def test_gives_up_after_max_retries():498"""Test that service stops retrying after max attempts."""499client = Mock()500client.request.side_effect = ConnectionError("Failed")501502service = ServiceWithRetry(client, max_retries=3)503504with pytest.raises(ConnectionError):505service.fetch()506507assert client.request.call_count == 3508509def test_does_not_retry_on_permanent_error():510"""Test that permanent errors are not retried."""511client = Mock()512client.request.side_effect = ValueError("Invalid input")513514service = ServiceWithRetry(client, max_retries=3)515516with pytest.raises(ValueError):517service.fetch()518519# Only called once - no retry for ValueError520assert client.request.call_count == 1521```522523### Mocking Time with Freezegun524525Use freezegun to control time in tests for predictable time-dependent behavior.526527```python528from freezegun import freeze_time529from datetime import datetime, timedelta530531@freeze_time("2026-01-15 10:00:00")532def test_token_expiry():533"""Test token expires at correct time."""534token = create_token(expires_in_seconds=3600)535assert token.expires_at == datetime(2026, 1, 15, 11, 0, 0)536537@freeze_time("2026-01-15 10:00:00")538def test_is_expired_returns_false_before_expiry():539"""Test token is not expired when within validity period."""540token = create_token(expires_in_seconds=3600)541assert not token.is_expired()542543@freeze_time("2026-01-15 12:00:00")544def test_is_expired_returns_true_after_expiry():545"""Test token is expired after validity period."""546token = Token(expires_at=datetime(2026, 1, 15, 11, 30, 0))547assert token.is_expired()548549def test_with_time_travel():550"""Test behavior across time using freeze_time context."""551with freeze_time("2026-01-01") as frozen_time:552item = create_item()553assert item.created_at == datetime(2026, 1, 1)554555# Move forward in time556frozen_time.move_to("2026-01-15")557assert item.age_days == 14558```559560### Test Markers561562```python563# test_markers.py564import pytest565566@pytest.mark.slow567def test_slow_operation():568"""Mark slow tests."""569import time570time.sleep(2)571572573@pytest.mark.integration574def test_database_integration():575"""Mark integration tests."""576pass577578579@pytest.mark.skip(reason="Feature not implemented yet")580def test_future_feature():581"""Skip tests temporarily."""582pass583584585@pytest.mark.skipif(os.name == "nt", reason="Unix only test")586def test_unix_specific():587"""Conditional skip."""588pass589590591@pytest.mark.xfail(reason="Known bug #123")592def test_known_bug():593"""Mark expected failures."""594assert False595596597# Run with:598# pytest -m slow # Run only slow tests599# pytest -m "not slow" # Skip slow tests600# pytest -m integration # Run integration tests601```602603### Coverage Reporting604605```bash606# Install coverage607pip install pytest-cov608609# Run tests with coverage610pytest --cov=myapp tests/611612# Generate HTML report613pytest --cov=myapp --cov-report=html tests/614615# Fail if coverage below threshold616pytest --cov=myapp --cov-fail-under=80 tests/617618# Show missing lines619pytest --cov=myapp --cov-report=term-missing tests/620```621622For advanced patterns (async testing, monkeypatching, property-based testing, database testing, CI/CD integration, and configuration), see [references/advanced-patterns.md](references/advanced-patterns.md)623