| name | python-testing-general |
| description | General standards for writing reliable, isolated pytest tests in Python projects. Use this skill for test structure, fixtures, mocking, and test organization patterns. |
| auto_load | {"enabled":true,"triggers":{"keywords":["pytest","test","mock","fixture","unittest","conftest"],"file_patterns":["test_*.py","*_test.py","**/tests/**/*.py","conftest.py"]},"priority":"high","load_with":["python-fundamentals"]} |
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_user_service.py
test_build_wrapper.py
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():
items = [Item(price=10), Item(price=20)]
result = calculate_total(items)
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)
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_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.
"""
if (Path.cwd() / 'expected_marker').is_dir():
return Path.cwd()
if _ROOT_FROM_SCRIPT.is_dir():
return _ROOT_FROM_SCRIPT
return None
Fixtures
Scope and Autouse
@pytest.fixture
def sample_data():
return {"key": "value"}
@pytest.fixture(scope="module")
def database_connection():
conn = create_connection()
yield conn
conn.close()
@pytest.fixture(scope="session")
def app_config():
return load_config()
@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
@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()
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
assert "error" in result
assert all(item.is_valid() for item in items)
assert response["status"] == "success"
assert "data" in response
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:
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
user = UserFactory()
inactive_user = UserFactory(is_active=False)
Running Tests
pytest
pytest tests/unit/test_services.py
pytest -v
pytest --cov=src --cov-report=term-missing
pytest -k "test_user"
pytest -n auto
pytest --profile
pytest -x
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() create class 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