home / skills / martinffx / claude-code-atelier / atelier-python-testing

This skill helps you implement stub-driven TDD and layer boundary tests for Python applications using pytest.

npx playbooks add skill martinffx/claude-code-atelier --skill atelier-python-testing

Review the files below or copy the command above to add this skill to your agents.

Files (4)
SKILL.md
4.6 KB
---
name: python:testing
description: Stub-Driven TDD and layer boundary testing with pytest. Use when writing tests, deciding what to test, testing at component boundaries, or implementing test-driven development.
user-invocable: false
---

# Testing with pytest

Stub-Driven TDD and layer boundary testing patterns for Python applications.

## Core Principle: Stub-Driven TDD

Test at component boundaries, not internal implementation:

```
Router → Service → Repository → Entity → Database
   ↓        ↓           ↓          ↓
 Test    Test        Test       Test
```

Follow the **Stub → Test → Implement → Refactor** workflow:

1. **Stub** - Create function signature with `pass`
2. **Test** - Write test for expected behavior
3. **Implement** - Make test pass
4. **Refactor** - Clean up code

```python
# 1. Stub
def calculate_discount(total: Decimal) -> Decimal:
    pass

# 2. Test
def test_discount_for_large_order():
    result = calculate_discount(Decimal("150"))
    assert result == Decimal("15")

# 3. Implement
def calculate_discount(total: Decimal) -> Decimal:
    if total > 100:
        return total * Decimal("0.1")
    return Decimal("0")
```

## Layer Boundary Testing Overview

Test **what crosses layer boundaries**, not internal implementation:

- **Entity Layer**: Domain logic, validation, transformations (from_request, to_response, to_record)
- **Service Layer**: Business workflows, error handling, dependency orchestration
- **Repository Layer**: CRUD operations, query logic, entity ↔ record transformations
- **Router Layer**: Request validation, response serialization, status codes

See references/boundaries.md for comprehensive layer-specific examples.

## Entity Testing Example

Test transformations and business logic:

```python
def test_product_from_request():
    """Test creation from request"""
    request = CreateProductRequest(name="Widget", price=Decimal("9.99"))
    product = Product.from_request(request)

    assert product.name == "Widget"
    assert product.price == Decimal("9.99")
    assert isinstance(product.id, UUID)

def test_product_apply_discount():
    """Test business logic"""
    product = Product(id=uuid4(), name="Widget", price=Decimal("100"))
    discounted = product.apply_discount(Decimal("0.1"))

    assert discounted.price == Decimal("90")
```

## Service Testing Example

Test orchestration with stubbed dependencies:

```python
from unittest.mock import Mock

def test_create_product_service():
    """Test with mocked repository"""
    mock_repo = Mock()
    mock_repo.save.return_value = Product(id=uuid4(), name="Widget")

    service = ProductService(repo=mock_repo)
    result = service.create(CreateProductRequest(name="Widget", price=Decimal("9.99")))

    mock_repo.save.assert_called_once()
    assert result.name == "Widget"
```

## Repository Testing Example

Test data access with real test database:

```python
@pytest.fixture
def test_db():
    """In-memory test database"""
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    return sessionmaker(bind=engine)()

def test_repository_save(test_db):
    """Test database operations"""
    repo = ProductRepository(test_db)
    product = Product(id=uuid4(), name="Widget", price=Decimal("9.99"))

    saved = repo.save(product)

    assert saved.id == product.id
    assert test_db.query(ProductRecord).count() == 1
```

## Router Testing Example

Test HTTP layer with TestClient:

```python
from fastapi.testclient import TestClient

def test_create_product_endpoint():
    """Test POST endpoint"""
    client = TestClient(app)

    response = client.post(
        "/products",
        json={"name": "Widget", "price": 9.99},
    )

    assert response.status_code == 201
    assert response.json()["name"] == "Widget"
```

## Test Organization Basics

```
tests/
├── unit/
│   ├── test_entities.py      # Entity + Value object tests
│   └── test_services.py      # Service tests (with mocks)
├── integration/
│   ├── test_repositories.py  # Repository tests (with DB)
│   └── test_endpoints.py     # Router tests (with client)
└── conftest.py               # Shared fixtures
```

## Reference Documentation

For comprehensive patterns and examples, see:

- **references/boundaries.md** - Layer boundary testing patterns with complete examples for each layer
- **references/mocking.md** - Mock strategies, verification methods, and anti-patterns
- **references/pytest.md** - Configuration, fixtures, markers, parametrization, and debugging

Progressive disclosure: SKILL.md provides quick reference, references/ contain full details.

Overview

This skill teaches Stub-Driven TDD and layer-boundary testing for Python projects using pytest. It focuses on writing minimal stubs, driving implementation with tests, and testing what crosses component boundaries rather than internal details. It provides patterns for entities, services, repositories, and HTTP routers to keep tests fast, reliable, and focused.

How this skill works

Start by stubbing a function or method signature, write a failing test for the expected behavior, implement the minimal code to make the test pass, then refactor. Tests are organized by layer: entity tests verify domain transformations and validation; service tests orchestrate behavior with mocked dependencies; repository tests run against a test database; router tests exercise request validation and response serialization via a test client. The approach emphasizes testing interactions at boundaries and using mocks only for external dependencies.

When to use it

  • When practicing Test-Driven Development on new features or bug fixes
  • When deciding which behavior belongs to which layer (entity/service/repository/router)
  • When you need fast unit tests for business logic and slower integration tests for data access or HTTP endpoints
  • When you want to reduce brittle tests by avoiding internal implementation assertions
  • When designing APIs where request/response transformation and status codes must be validated

Best practices

  • Follow Stub → Test → Implement → Refactor: start with a no-op stub, write a focused test, implement minimal behavior, then clean up
  • Test only what crosses layer boundaries: validate transformations, orchestration, persistence, and HTTP contracts rather than internals
  • Mock dependencies at service level to verify orchestration and interactions, but use a real test database for repository tests
  • Keep router tests at the HTTP layer using a TestClient to assert status codes and payloads
  • Organize tests by unit/integration boundaries and share fixtures in a central conftest to avoid duplication

Example use cases

  • Entity tests for from_request/to_response transformations and domain business rules (e.g., apply_discount)
  • Service tests that assert calls to repositories and error handling using unittest.mock or similar
  • Repository integration tests against an in-memory or test database to validate CRUD and mapping logic
  • Router endpoint tests with TestClient to verify validation, status codes, and serialized responses
  • TDD flow when introducing a new API endpoint: stub handler, write router test, implement service, then repository

FAQ

Should I mock the database in repository tests?

No. Repository tests should use a test database (in-memory or a dedicated test instance) to validate queries and mappings; mocking hides real persistence behavior.

When is it appropriate to mock dependencies?

Mock dependencies at the service layer to test orchestration and interaction counts. Avoid mocking inside entities and repository internals; instead test those layers directly with real inputs or a test DB.