clean-pytest

ClawSkills 作者 clawskills

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-pytest
cURL直接下载,无需登录
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]