clean-pytest
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.
安装 / 下载方式
TotalClaw CLI推荐
totalclaw install clawskills:clawskills~marcoracer-clean-pytestcURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/clawskills%3Aclawskills~marcoracer-clean-pytest/file -o marcoracer-clean-pytest.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
- Testing layered architectures (Controllers → Services → Repositories)
- Writing parametrized tests for multiple scenarios
## 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?**
- More readable and maintainable
- Easier to debug
- Better test isolation
- No monkey-patching magic
- Self-documenting behavior
### 2. Explicit AAA Pattern
Structure every test into three clear phases with comments:
```python
# 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:
```python
# tests/fakes.py
from typing import Any, Dict, List, Optional
class 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
```python
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
```python
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
```python
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)
```python
# 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
```python
@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
```python
@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
```python
@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
```python
@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
```python
# tests/test_user_service.py
import pytest
from myapp.services.user_service import UserService
def 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
```python
@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
```python
@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
```python
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]