Clean Pytest
by @marcoracer
Write clean, maintainable pytest tests using Fake-based testing, contract testing, and dependency injection patterns. Use when setting up test suites for Pyt...
clawhub install clean-pytestπ About This Skill
name: clean-pytest description: 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. license: MIT metadata: emoji: "π§ͺ" homepage: https://github.com/numinstante/skills os: [darwin, linux, windows] tags: [python, pytest, testing, tdd, mcp, contract-testing] requires: anyBins: [python3]
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
Core Principles
1. Fakes over Mocks
Use Fake classes instead of mocking with unittest.mock. Fakes are in-memory implementations that mimic real dependencies without external calls.
Why Fakes?
2. Explicit AAA Pattern
Structure every test into three clear phases with comments:
# Arrange
Set up test data and dependencies
Act
Execute the code under test
Assert
Verify the result
3. Dependency Injection in Fixtures
Inject dependencies between fixtures to maintain relationships and avoid duplication.
4. Contract Testing
Verify that components register tools/functions correctly and pass expected arguments.
Architecture Pattern
Controller (MCP Tools)
β
Service (Business Logic)
β
Repository (Data Access)
β
Fake (Test Implementation)
Creating Fakes
Basic Fake Structure
Create a Fake class that implements the same interface as the real dependency:
# tests/fakes.py
from typing import Any, Dict, List, Optionalclass FakeAuth:
"""Fake implementation of AuthProvider for testing."""
def __init__(self) -> None:
self.created: List[Dict[str, Any]] = []
self.deleted: List[str] = []
self._seq = 0
self.fail_on_create: bool = False
def create_user(self, email: str, password: str, display_name: str) -> str:
if self.fail_on_create:
raise RuntimeError("create_user failed (fake)")
self._seq += 1
uid = f"uid-{self._seq}"
rec = {"uid": uid, "email": email, "display_name": display_name}
self.created.append(rec)
return uid
def delete_user(self, uid: str) -> None:
self.deleted.append(uid)
Repository Fake
class FakeUsersRepo:
"""Fake implementation of UsersRepository."""
def __init__(self) -> None:
self.users: Dict[str, Dict[str, Any]] = {}
self.fail_on_upsert: bool = False def upsert_user_doc(self, uid: str, data: Dict[str, Any]) -> None:
if self.fail_on_upsert:
raise RuntimeError("upsert_user_doc failed (fake)")
self.users[uid] = dict(data)
def list_users(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
items = list(self.users.values())
if limit and limit > 0:
items = items[:limit]
return [dict(it) for it in items]
Controlled Failure Fakes
class FakeAuth:
def __init__(self) -> None:
self.fail_on_create: bool = False # Control failure in tests def create_user(self, email: str, password: str, display_name: str) -> str:
if self.fail_on_create:
raise RuntimeError("create_user failed (fake)")
# ... rest of implementation
Nested Repository Fakes
class FakeSectorsRepo:
def __init__(self, institutions: FakeInstitutionsRepo | None = None) -> None:
self.institutions = institutions # Inject dependency
self.data: Dict[str, Dict[str, Dict[str, Any]]] = {} def institution_exists(self, institution_id: str) -> bool:
return bool(self.institutions and institution_id in self.institutions.data)
def upsert_sector(self, institution_id: str, sector_id: str, data: Dict[str, Any]) -> None:
self.data.setdefault(institution_id, {})[sector_id] = dict(data)
Fixtures
Basic Fixture (conftest.py)
# 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
Seeded Environment Fixture
@pytest.fixture()
def user_env_seeded(user_env):
"""Environment with pre-seeded data."""
svc, auth, repo = user_env
svc.add_user(email="test@example.com", password="secret", name="Test User")
return svc
Fixture with Cleanup
@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)
Service Layer Testing
Basic AAA Pattern Test
# tests/test_user_service.py
import pytest
from myapp.services.user_service import UserServicedef test_add_user_success(fake_auth, fake_users_repo):
# Arrange
svc = UserService(fake_auth, fake_users_repo)
email = "test@example.com"
password = "secret"
name = "Test User"
# Act
result = svc.add_user(email=email, password=password, name=name)
# Assert
assert result["status"] == "ok"
assert result["user"]["email"] == email
assert result["user"]["name"] == name
assert result["uid"] in fake_users_repo.users
Parametrized Tests
@pytest.mark.parametrize(
"email,password,name,role",
[
("a@example.com", "secret", "Alice", "admin"),
("b@example.com", "p@ss", "Bob", "user"),
],
)
def test_add_user_parametrized(user_env, email, password, name, role):
svc, _auth, _repo = user_env # Act
res = svc.add_user(email=email, password=password, name=name, global_role=role)
# Assert
assert res["status"] == "ok"
assert res["user"]["email"] == email
assert res["user"]["name"] == name
assert res["user"]["globalRole"] == role
Testing Error Scenarios with Fakes
@pytest.mark.parametrize("email", ["c@example.com", "d@example.com"])
def test_add_user_rollback_on_firestore_failure(fake_auth, fake_users_repo, email):
# Arrange
fake_users_repo.fail_on_upsert = True
svc = UserService(fake_auth, fake_users_repo) # Act & Assert
with pytest.raises(RuntimeError):
svc.add_user(email=email, password="secret", name="Bob")
# Assert rollback
assert fake_auth.deleted, "Expected auth user to be deleted on Firestore failure"
Testing Timestamp Normalization
def test_list_users_normalizes_timestamps_to_iso(user_env):
# Arrange
svc, _auth, repo = user_env
from datetime import datetime
repo.users["u1"] = {
"id": "u1",
"email": "x@y.z",
"name": "X",
"globalRole": "user",
"createdAt": datetime(2024, 1, 1),
"updatedAt": datetime(2024, 1, 2),
} # Act
res = svc.list_users(limit=10)
# Assert
assert res["status"] == "ok"
assert res["count"] == 1
user = res["users"][0]
assert isinstance(user["createdAt"], str)
assert isinstance(user["updatedAt"], str)
Contract Testing
MCP Tool Registration Contract
Test that controllers properly register tools with expected signatures:
# tests/test_controllers_contract.py
from typing import Any, Callable, Dictclass FakeMCP:
"""Minimal FakeMCP for contract testing."""
def __init__(self) -> None:
self.tools: Dict[str, Callable[..., Any]] = {}
self.meta: Dict[str, Dict[str, Any]] = {}
def tool(self, name: str, description: str, tags: Optional[set] = None, meta: Optional[dict] = None):
def decorator(fn: Callable[..., Any]):
self.tools[name] = fn
self.meta[name] = {
"description": description,
"tags": set(tags or set()),
"meta": dict(meta or {}),
}
return fn
return decorator
class FakeUserService:
"""Simple fake service that records calls."""
def __init__(self):
self.calls = []
def add_user(self, **kwargs):
self.calls.append(("add_user", kwargs))
return {"status": "ok", "op": "add_user", "args": kwargs}
def test_users_controller_contract():
# Arrange
from myapp.controllers.users_controller import UsersController
fake = FakeMCP()
svc = FakeUserService()
UsersController(fake, svc)
# Assert tool registration
assert "add_user" in fake.tools
assert "list_users" in fake.tools
# Act & Assert tool behavior
res = fake.tools"add_user"
assert res["status"] == "ok"
assert res["op"] == "add_user"
assert res["args"]["email"] == "a@x.y"
Parametrized Contract Tests
@pytest.mark.parametrize(
"email,password,name,role",
[
("a@x.y", "s3cr3t", "Alice", "admin"),
("b@x.y", "p@ssw0rd", "Bob", "user"),
],
)
def test_users_add_user_parametrized(_users_env, email, password, name, role):
# Arrange
fake, _ = _users_env # Act
res = fake.tools"add_user"
# Assert
assert res["status"] == "ok"
assert res["op"] == "add_user"
assert res["args"]["email"] == email
Repository Layer Testing
Testing Repository Operations
@pytest.fixture()
def repo_env(fake_institutions_repo, fake_sectors_repo):
# Seed data
fake_institutions_repo.upsert("inst1", {"id": "inst1", "name": "Inst One"})
fake_sectors_repo.upsert_sector(
"inst1", "er", {"id": "er", "name": "ER", "slug": "er", "isActive": True}
)
return fake_sectors_repo
Testing Multiple Data Scenarios
@pytest.mark.parametrize("rooms", [
["101"],
["201", {"name": "102", "id": "room-102"}],
])
def test_add_and_list_rooms(room_env, rooms):
svc, _ = room_env # Act
res = svc.add_sector_rooms("inst1", "er", rooms)
# Assert
assert res["status"] == "ok"
assert res["count"] == len(rooms)
lst = svc.list_sector_rooms("inst1", "er", limit=10)
assert lst["status"] == "ok"
assert lst["count"] == len(rooms)
Testing Limit Behavior
@pytest.mark.parametrize("limit", [1, 3])
def test_list_rooms_limits(room_env_seeded, limit):
svc = room_env_seeded # Act
lst = svc.list_sector_rooms("inst1", "er", limit=limit)
# Assert
assert lst["status"] == "ok"
assert lst["count"] == min(2, limit) # 2 items seeded
Testing Not Found Scenarios
@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 pytestGate 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="test1@example.com", 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
β Good - Use Fake for your interface:
def test_add_user(fake_auth, fake_users_repo):
svc = UserService(fake_auth, fake_users_repo)
# ... test code
Don't Test Implementation Details
β Bad - Testing internal method calls:
def test_add_user(fake_auth, fake_users_repo):
svc = UserService(fake_auth, fake_users_repo)
svc.add_user(email="test@example.com", password="secret", name="User")
assert fake_auth.created == [{"uid": "uid-1", ...}] # Implementation detail
β Good - Testing observable behavior:
def test_add_user(fake_auth, fake_users_repo):
svc = UserService(fake_auth, fake_users_repo)
result = svc.add_user(email="test@example.com", password="secret", name="User")
assert result["status"] == "ok"
assert result["user"]["email"] == "test@example.com"
Don't Skip Error Paths
β Bad - Only happy path:
def test_add_user_success(fake_auth, fake_users_repo):
# Only tests success case
β Good - Test all scenarios:
def test_add_user_success(fake_auth, fake_users_repo):
# Happy pathdef test_add_user_rollback_on_firestore_failure(fake_auth, fake_users_repo):
# Error path
def test_add_user_handles_duplicate_email(fake_auth, fake_users_repo):
# Edge case
Running Tests
# Run all tests
pytestRun with coverage
pytest --cov=myapp --cov-report=term-missingRun specific test file
pytest tests/test_user_service.pyRun specific test
pytest tests/test_user_service.py::test_add_user_successRun parametrized tests with verbose output
pytest -v tests/test_user_service.py::test_add_user_parametrizedSkip integration tests
pytest -m "not integration"Run only integration tests
pytest -m integrationStop on first failure
pytest -xShow local variables on failure
pytest -lRun tests in parallel (with pytest-xdist)
pytest -n auto
Best Practices Checklist
unittest.mock