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.
Resources
1Install
npx skillscat add rory-data/copilot/python-testing-patterns Install via the SkillsCat registry.
SKILL.md
Python Testing Patterns
Master pytest fundamentals through essential patterns and practical examples, from basic unit tests to mocking external dependencies.
When to Use This Skill
- Writing unit tests for Python code
- Setting up test suites and test infrastructure
- Implementing test-driven development (TDD)
- Creating integration tests for APIs and services
- Mocking external dependencies and services
- Testing async code and concurrent operations
- Setting up continuous testing in CI/CD
- Implementing property-based testing
- Testing database operations
- Debugging failing tests
Core Concepts
1. Test Types
- Unit Tests: Test individual functions/classes in isolation
- Integration Tests: Test interaction between components
- Functional Tests: Test complete features end-to-end
- Performance Tests: Measure speed and resource usage
2. Test Structure (AAA Pattern)
- Arrange: Set up test data and preconditions
- Act: Execute the code under test
- Assert: Verify the results
3. Test Coverage
- Measure what code is exercised by tests
- Identify untested code paths
- Aim for meaningful coverage, not just high percentages
4. Test Isolation
- Tests should be independent
- No shared state between tests
- Each test should clean up after itself
Quick Start
# test_example.py
def add(a, b):
return a + b
def test_add():
"""Basic test example."""
result = add(2, 3)
assert result == 5
def test_add_negative():
"""Test with negative numbers."""
assert add(-1, 1) == 0
# Run with: uv run pytest test_example.pyFundamental Patterns
Pattern 1: Basic pytest Tests
# test_calculator.py
import pytest
class Calculator:
"""Simple calculator for testing."""
def add(self, a: float, b: float) -> float:
return a + b
def divide(self, a: float, b: float) -> float:
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def test_addition():
"""Test addition."""
calc = Calculator()
assert calc.add(2, 3) == 5
assert calc.add(-1, 1) == 0
def test_division_by_zero():
"""Test division by zero raises error."""
calc = Calculator()
with pytest.raises(ValueError, match="Cannot divide by zero"):
calc.divide(5, 0)Pattern 2: Fixtures for Setup and Teardown
# test_database.py
import pytest
@pytest.fixture
def db():
"""Fixture that provides connected database."""
# Setup
database = {"connected": True}
yield database
# Teardown
database["connected"] = False
def test_database_query(db):
"""Test using fixture."""
assert db["connected"] is TruePattern 3: Parametrised Tests
# test_validation.py
import pytest
@pytest.mark.parametrize("email,expected", [
("user@example.com", True),
("invalid.email", False),
])
def test_email_validation(email, expected):
"""Test email validation with various inputs."""
assert is_valid(email) == expectedPattern 4: Mocking with unittest.mock
# test_api_client.py
import pytest
from unittest.mock import Mock, patch
def test_api_call():
"""Test API call with mock."""
mock_response = Mock()
mock_response.json.return_value = {"id": 1}
with patch("requests.get", return_value=mock_response):
result = get_user(1)
assert result["id"] == 1Pattern 5: Testing Exceptions
# test_exceptions.py
import pytest
def test_exception():
"""Test exception is raised."""
with pytest.raises(ValueError, match="error message"):
divide(10, 0)Advanced & Specialised Patterns
For additional testing patterns and advanced techniques, see the reference files:
- Logging, Async, Fixtures & more: Patterns 6-12 cover specialised testing scenarios, including logging, async, and data validation
- Test Debugging: Debugging techniques and pytest commands
- Type-Safe Testing: Protocol-based type checking for mocks
- Configuration & Dependencies: Coverage reporting, database testing, CI/CD, pytest configuration
- Testing Anti-Patterns: Common pitfalls to avoid (production pollution, mock testing, etc.)
Testing Best Practices
Test Organisation
# tests/
# __init__.py
# conftest.py # Shared fixtures
# test_unit/ # Unit tests
# test_integration/ # Integration tests
# test_e2e/ # End-to-end testsTest Naming
# Good test names describe what is tested and expected outcome
def test_user_creation_with_valid_data():
pass
def test_login_fails_with_invalid_password():
pass
# Bad test names
def test_1(): # Not descriptive
pass
def test_user(): # Too vague
passTest Markers
import pytest
@pytest.mark.slow
def test_slow_operation():
pass
@pytest.mark.integration
def test_database_integration():
pass
# Run with:
# uv run pytest -m slow # Run only slow tests
# uv run pytest -m "not slow" # Skip slow testsBest Practices Summary
- Write tests first (TDD) or alongside code
- One assertion per test when possible
- Use descriptive test names that explain behaviour
- Keep tests independent and isolated
- Use fixtures for setup and teardown
- Mock external dependencies appropriately
- Parametrise tests to reduce duplication
- Test edge cases and error conditions
- Measure coverage but focus on quality
- Run tests in CI/CD on every commit
Quick Reference
- pytest documentation: https://docs.pytest.org/
- unittest.mock: https://docs.python.org/3/library/unittest.mock.html
- hypothesis: Property-based testing library
- pytest-asyncio: Testing async code
- pytest-cov: Coverage measurement and reporting
- pytest-mock: Enhanced mocking with spec support
- pydantic: Data validation and schema testing
- pandera: Schema validation for pandas DataFrames