python-testing-general
SKILL.md
Python Testing
Standards for writing reliable, isolated pytest tests in Python projects. Use this skill for test structure, fixtures, mocking, and test organization patterns.
When to Use This Skill
- Writing pytest tests for Python code
- Creating test fixtures and helpers
- Mocking external dependencies
- Organizing test files and directories
- Implementing parametrized tests
- Testing async code
Test Structure
Naming Conventions
# Test files: test_<module>.py
test_user_service.py
test_build_wrapper.py
# Test functions: test_<behavior>
def test_detect_wrapper_finds_unix_on_unix():
...
def test_returns_none_when_missing():
...
AAA Pattern
Structure tests with Arrange-Act-Assert:
def test_calculate_total():
# Arrange
items = [Item(price=10), Item(price=20)]
# Act
result = calculate_total(items)
# Assert
assert result == 30
Test Isolation
Working Directory Restoration
Tests that change cwd must restore it. Use an autouse fixture as a safety net:
import os
import pytest
@pytest.fixture(autouse=True)
def _restore_cwd():
"""Restore cwd after each test to prevent pollution."""
original_cwd = os.getcwd()
yield
if os.getcwd() != original_cwd:
os.chdir(original_cwd)
For explicit cwd changes within a test, use monkeypatch:
def test_script_in_different_directory(monkeypatch, tmp_path):
monkeypatch.chdir(tmp_path)
# Test runs with tmp_path as cwd
# Automatically restored after test
Temporary Directories
Use tmp_path for isolated file operations:
def test_creates_output_file(tmp_path):
output = tmp_path / "result.json"
generate_report(output)
assert output.exists()
Script Path Discovery
Scripts using Path.cwd() break when tests run from different directories. Use dual-path discovery:
from pathlib import Path
# Script-relative path (works regardless of cwd)
SCRIPT_DIR = Path(__file__).resolve().parent
_ROOT_FROM_SCRIPT = SCRIPT_DIR.parent.parent.parent
def find_project_root() -> Path | None:
"""Find root with cwd-first, script-relative fallback.
cwd-first allows tests to use fixture directories.
Script-relative fallback works when cwd is different.
"""
# Check cwd-based paths first (supports test fixtures)
if (Path.cwd() / 'expected_marker').is_dir():
return Path.cwd()
# Fallback to script-relative (works regardless of cwd)
if _ROOT_FROM_SCRIPT.is_dir():
return _ROOT_FROM_SCRIPT
return None
Fixtures
Scope and Autouse
# Function scope (default) - runs for each test
@pytest.fixture
def sample_data():
return {"key": "value"}
# Module scope - runs once per test file
@pytest.fixture(scope="module")
def database_connection():
conn = create_connection()
yield conn
conn.close()
# Session scope - runs once per test session
@pytest.fixture(scope="session")
def app_config():
return load_config()
# Autouse - runs automatically for every test
@pytest.fixture(autouse=True)
def _clear_cache():
cache.clear()
yield
Fixture with Cleanup
@pytest.fixture
def temp_database():
db = create_test_database()
yield db
db.destroy()
Parametrized Fixtures
@pytest.fixture(params=[True, False])
def debug_mode(request):
return request.param
Parametrization
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("world", "WORLD"),
("", ""),
])
def test_uppercase(input, expected):
assert input.upper() == expected
# Multiple parameters
@pytest.mark.parametrize("email,valid", [
("test@example.com", True),
("invalid", False),
("", False),
("a@b.c", True),
])
def test_validate_email(email: str, valid: bool):
result = validate_email(email)
assert result == valid
Mocking
Patching Module State
from unittest.mock import patch
def test_platform_detection():
with patch('module.IS_WINDOWS', True):
result = detect_wrapper()
assert 'bat' in result
Patching Functions
def test_fallback_to_system(tmp_path):
with patch('shutil.which', return_value='/usr/bin/tool'):
result = detect_wrapper(str(tmp_path), 'tool', 'tool.bat', 'tool')
assert result == 'tool'
Async Mocking
from unittest.mock import AsyncMock, patch
@pytest.mark.asyncio
async def test_create_user_sends_email():
with patch('app.services.send_email', new_callable=AsyncMock) as mock:
user = await create_user(UserCreate(email="test@example.com"))
mock.assert_called_once()
# Using AsyncMock directly
async def test_async_function():
mock = AsyncMock(return_value=User(id=1))
result = await mock()
Mocking with pytest-mock
def test_with_mocker(mocker):
mocker.patch('module.function', return_value='mocked')
result = module.function()
assert result == 'mocked'
def test_spy(mocker):
mocker.spy(module, 'function')
module.function()
module.function.assert_called_once()
Assertions
Basic Assertions
assert result == expected
assert item in collection
assert value is None
assert len(items) == 3
assert response.status_code == 200
Exception Testing
import pytest
def test_raises_on_invalid_input():
with pytest.raises(ValueError, match="must be positive"):
process_value(-1)
def test_raises_specific_exception():
with pytest.raises(NotFoundError) as exc_info:
get_resource(999)
assert exc_info.value.resource == "user"
Approximate Comparisons
assert result == pytest.approx(3.14159, rel=1e-3)
Asserting on Collections
# Check contents
assert "error" in result
assert all(item.is_valid() for item in items)
# Dictionary assertions
assert response["status"] == "success"
assert "data" in response
# Using pytest assertions
from pytest import asserts
asserts.assert_equal(result, expected)
asserts.assert_in("key", data)
Async Testing
Basic Async Tests
import pytest
@pytest.mark.asyncio
async def test_async_fetch():
result = await fetch_data("url")
assert result is not None
Async Fixtures
import pytest
from httpx import AsyncClient
@pytest.fixture
async def client(app):
async with AsyncClient(app=app, base_url="http://test") as client:
yield client
@pytest.fixture
async def db_session():
async with async_session() as session:
yield session
await session.rollback()
Concurrent Async Tests
import asyncio
@pytest.mark.asyncio
async def test_concurrent_requests():
results = await asyncio.gather(
fetch_data("url1"),
fetch_data("url2"),
)
assert len(results) == 2
Test Organization
Shared Infrastructure
Place shared fixtures and helpers in conftest.py:
# test/conftest.py
import pytest
@pytest.fixture
def sample_config():
return {"debug": True}
def run_script(script_path, *args):
"""Helper to run scripts with subprocess."""
...
Test File Structure
test/
├── conftest.py # Shared fixtures
├── bundle_name/
│ ├── conftest.py # Bundle-specific fixtures
│ ├── test_feature.py
│ └── test_integration.py
├── unit/
│ ├── test_services.py
│ └── test_models.py
└── integration/
├── test_api.py
└── test_database.py
Factory Pattern for Tests
from factory import Factory, Faker
class UserFactory(Factory):
class Meta:
model = User
email = Faker('email')
name = Faker('name')
is_active = True
# Usage
user = UserFactory()
inactive_user = UserFactory(is_active=False)
Running Tests
# Run all tests
pytest
# Run specific module
pytest tests/unit/test_services.py
# Run with verbose output
pytest -v
# Run with coverage
pytest --cov=src --cov-report=term-missing
# Run tests matching pattern
pytest -k "test_user"
# Run tests in parallel
pytest -n auto
# Run with profiling
pytest --profile
# Stop on first failure
pytest -x
# Run tests matching name pattern
pytest --test-name-pattern="test_user"
Test Organization Best Practices
- Mirror source structure - Tests should mirror
src/directory structure - Test methods start with
test_- pytest convention - Use test class suites - For
def foo()createclass TestFoo - Keep names concise - Omit class suite name from method
- Check for tests when changing code - Always ensure appropriate unit tests
Quality Guidelines
- Use AAA pattern - Arrange, Act, Assert
- Tests should be useful - Readable, concise, maintainable
- Avoid massive diffs - Don't create tests that become burdensome
- Mock external dependencies - Database, API calls, file system
- Use meaningful assertions - Test behavior, not implementation
- One assertion per test - Or few related assertions
- Independent tests - No order dependency between tests
References
Additional References
For production-ready patterns beyond this guide, see:
- Pytest Fixtures Cookbook - Token bucket rate limiter, retry with exponential backoff, connection pools, batch processors, event bus, transaction context managers, async cache with TTL, graceful shutdown handlers
Weekly Installs
3
Repository
amrahman90/pyth…rt-agentGitHub Stars
2
First Seen
9 days ago
Security Audits
Installed on
mcpjam3
iflow-cli3
claude-code3
junie3
windsurf3
zencoder3