Write clean, maintainable pytest tests using Fake-based testing, contract testing, and dependency injection patterns. Use when setting up test suites for Python/MCP projects, creating Fakes for external dependencies, writing contract tests, or implementing test patterns with fixtures and parametrization.
SKILL.md
Clean Pytest
Clean, maintainable pytest test patterns using Fake-based testing, contract testing, and dependency injection. Focuses on test isolation, reusability, and clarity through explicit AAA pattern and well-structured fixtures.
When to Use
Setting up test suites for Python/MCP projects
Creating Fake implementations for external dependencies
Writing contract tests for MCP tools/controllers
Implementing test patterns with dependency injection
# tests/conftest.py
import pytest
from tests.fakes import FakeAuth, FakeUsersRepo
@pytest.fixture()
def fake_auth():
"""Provide a fresh FakeAuth for each test."""
return FakeAuth()
@pytest.fixture()
def fake_users_repo():
"""Provide a fresh FakeUsersRepo for each test."""
return FakeUsersRepo()
Fixture with Dependency Injection
@pytest.fixture()
def fake_sectors_repo(fake_institutions_repo):
"""FakeSectorsRepo depends on FakeInstitutionsRepo."""
return FakeSectorsRepo(institutions=fake_institutions_repo)
@pytest.fixture()
def fake_rooms_repo(fake_sectors_repo):
"""FakeRoomsRepo depends on FakeSectorsRepo."""
return FakeRoomsRepo(sectors=fake_sectors_repo)
Environment Fixture
@pytest.fixture()
def user_env(fake_auth, fake_users_repo):
"""Provide service and all dependencies for user operations."""
from myapp.services.user_service import UserService
svc = UserService(fake_auth, fake_users_repo)
return svc, fake_auth, fake_users_repo
@pytest.fixture()
def temp_file():
"""Provide a temporary file and clean up after test."""
import tempfile
import os
fd, path = tempfile.mkstemp()
os.close(fd)
yield path
os.unlink(path)
@pytest.mark.parametrize("room_id,deleted", [
("room-102", True),
("room-999", False),
])
def test_remove_rooms_parametrized(room_env_seeded, room_id, deleted):
svc = room_env_seeded
# Act
res = svc.remove_sector_room("inst1", "er", room_id)
# Assert
assert res["deleted"] is deleted
if not deleted:
assert res.get("reason") == "room_not_found"
Integration Testing
Conditional Integration Tests
Skip integration tests when external dependencies are not available:
# tests/test_integration_wiring.py
import os
import pytest
# Gate this integration test on presence of credentials
_ENV_KEYS = (
"FIREBASE_SERVICE_ACCOUNT",
"GOOGLE_APPLICATION_CREDENTIALS",
)
_has_env_creds = any(os.getenv(k) for k in _ENV_KEYS)
pytestmark = [
pytest.mark.integration,
pytest.mark.skipif(
not _has_env_creds,
reason=(
"Integration test requires Firebase Admin credentials via env "
"(FIREBASE_SERVICE_ACCOUNT or GOOGLE_APPLICATION_CREDENTIALS)"
),
),
]
@pytest.mark.integration
def test_build_app_initializes_and_registers_tools():
# Arrange
from myapp.wiring import build_app
# Act
app = build_app()
# Assert
assert hasattr(app, "run")
Test Isolation
Each test should be independent and not share state:
def test_user_created_in_one_test_not_visible_in_another(fake_auth, fake_users_repo):
# Arrange
svc1 = UserService(fake_auth, fake_users_repo)
# Act
result1 = svc1.add_user(email="[email protected]", password="secret", name="User1")
# Assert - second test with fresh fixtures should not see this user
svc2 = UserService(fake_auth, fake_users_repo)
users = svc2.list_users()
assert users["count"] == 1 # Only the user from this test
Testing Anti-Patterns to Avoid
Don't Mock What You Don't Own
❌ Bad - Mocking external library:
@patch('firebase_admin.auth.create_user')
def test_add_user(mock_create_user):
mock_create_user.return_value = Mock(uid="uid-1")
# ... test code
# Run all tests
pytest
# Run with coverage
pytest --cov=myapp --cov-report=term-missing
# Run specific test file
pytest tests/test_user_service.py
# Run specific test
pytest tests/test_user_service.py::test_add_user_success
# Run parametrized tests with verbose output
pytest -v tests/test_user_service.py::test_add_user_parametrized
# Skip integration tests
pytest -m "not integration"
# Run only integration tests
pytest -m integration
# Stop on first failure
pytest -x
# Show local variables on failure
pytest -l
# Run tests in parallel (with pytest-xdist)
pytest -n auto