1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
# Python Testing Rules
Applies to: `**/*.py`
Implements the core principles from `testing.md`. All rules there apply here —
this file covers Python-specific patterns.
## Framework: pytest (NEVER unittest)
Use `pytest` for all Python tests. Do not use `unittest.TestCase` unless
integrating with legacy code that requires it.
## Test Structure
Group tests in classes that mirror the source module:
```python
class TestCartService:
"""Tests for CartService."""
@pytest.fixture
def cart(self):
return Cart(user_id=42)
def test_add_item_normal(self, cart):
"""Normal: adding an in-stock item increases quantity."""
cart.add("SKU-1", quantity=2)
assert cart.item_count("SKU-1") == 2
def test_add_item_boundary_zero_quantity(self, cart):
"""Boundary: quantity 0 is a no-op, not an error."""
cart.add("SKU-1", quantity=0)
assert cart.item_count("SKU-1") == 0
def test_add_item_error_negative(self, cart):
"""Error: negative quantity raises ValueError."""
with pytest.raises(ValueError, match="quantity must be non-negative"):
cart.add("SKU-1", quantity=-1)
```
## Fixtures Over Factories
- Use `pytest` fixtures for test data setup
- Use `@pytest.fixture(autouse=True)` sparingly — prefer explicit injection
- Avoid `factory_boy` unless object graphs are genuinely complex
- Django: prefer pytest fixtures over `setUpTestData` unless you have a
performance reason
## Parametrize for Category Coverage
Use `@pytest.mark.parametrize` to cover normal, boundary, and error cases
concisely instead of hand-writing near-duplicate tests:
```python
@pytest.mark.parametrize("quantity,valid", [
(1, True), # Normal
(100, True), # Normal: bulk
(0, True), # Boundary: zero is a no-op
(-1, False), # Error: negative
])
def test_add_item_quantity_validation(cart, quantity, valid):
if valid:
cart.add("SKU-1", quantity=quantity)
else:
with pytest.raises(ValueError):
cart.add("SKU-1", quantity=quantity)
```
### Pairwise / Combinatorial for Parameter-Heavy Functions
When `@pytest.mark.parametrize` would require listing dozens of combinations
(feature flags × permissions × shipping × payment × etc.), switch to
combinatorial coverage via `/pairwise-tests`. The skill generates a minimal
matrix covering every 2-way parameter interaction — typically 80-99% fewer
cases than exhaustive, catching most combinatorial bugs.
Workflow: invoke `/pairwise-tests` → get a PICT model + generated test matrix
→ paste the matrix into a pytest parametrize block, or use the helper to
emit directly. The `pypict` package (`pip install pypict`) handles
generation in-process.
See `testing.md` § Combinatorial Coverage for the general rule and when
to skip.
## Mocking Guidelines
### Mock these (external boundaries):
- External APIs (`requests`, `httpx`, `boto3` clients)
- Time (`freezegun` or `time-machine`)
- File uploads (Django: `SimpleUploadedFile`)
- Celery tasks (`@override_settings(CELERY_ALWAYS_EAGER=True)`)
- Email sending (Django: `django.core.mail.outbox`)
### Never mock these (internal domain):
- ORM queries (SQLAlchemy, Django ORM)
- Model methods and properties
- Form and serializer validation
- Middleware
- Your own service functions
## Async Testing
Use `anyio` for async tests (not raw `asyncio`):
```python
@pytest.mark.anyio
async def test_process_order_async():
result = await process_order_async(sample_order)
assert result.status == "processed"
```
## Database Testing (Django)
- Mark database tests with `@pytest.mark.django_db`
- Use transactions for isolation (pytest-django default)
- Prefer in-memory SQLite for speed in unit tests
- Use `select_related` / `prefetch_related` assertions to catch N+1 regressions
|