@martinffx/atelier 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/agents/architect.md +38 -0
- package/agents/oracle.md +40 -0
- package/agents/recon.md +39 -0
- package/dist/atelier.js +33082 -0
- package/package.json +44 -0
- package/skills/code-conventional-commit/SKILL.md +134 -0
- package/skills/code-conventional-commit/evals/evals.json +23 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/commit-message.txt +4 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/validation-result.txt +16 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/commit-message.txt +8 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/report.html +1325 -0
- package/skills/code-debug/SKILL.md +271 -0
- package/skills/code-debug/references/language-tools.md +139 -0
- package/skills/code-debug/references/techniques.md +117 -0
- package/skills/code-docs/SKILL.md +171 -0
- package/skills/code-docs/references/api-docs.md +116 -0
- package/skills/code-docs/references/changelog.md +64 -0
- package/skills/code-docs/references/readme-structure.md +94 -0
- package/skills/code-git-worktrees/SKILL.md +218 -0
- package/skills/code-perf/SKILL.md +163 -0
- package/skills/code-perf/references/common-bottlenecks.md +124 -0
- package/skills/code-perf/references/optimization-patterns.md +167 -0
- package/skills/code-perf/references/profiling-tools.md +126 -0
- package/skills/code-review/SKILL.md +78 -0
- package/skills/code-review/references/output.md +200 -0
- package/skills/code-review/references/reviewers.md +719 -0
- package/skills/code-review/references/rq.md +357 -0
- package/skills/code-review/references/rs.md +30 -0
- package/skills/code-security/SKILL.md +183 -0
- package/skills/code-security/references/owasp-top-10.md +129 -0
- package/skills/code-security/references/security-tools.md +102 -0
- package/skills/code-security/references/vulnerability-patterns.md +129 -0
- package/skills/code-stacked-commit/SKILL.md +155 -0
- package/skills/code-subagents/SKILL.md +184 -0
- package/skills/code-subagents/references/code-quality-reviewer-prompt.md +48 -0
- package/skills/code-subagents/references/implementor-prompt.md +79 -0
- package/skills/code-subagents/references/spec-reviewer-prompt.md +62 -0
- package/skills/oracle-architect/SKILL.md +246 -0
- package/skills/oracle-architect/references/api-design.md +345 -0
- package/skills/oracle-architect/references/data-modeling.md +318 -0
- package/skills/oracle-architect/references/ddd-patterns.md +247 -0
- package/skills/oracle-challenge/SKILL.md +126 -0
- package/skills/oracle-testing/SKILL.md +237 -0
- package/skills/oracle-testing/references/anti-patterns.md +618 -0
- package/skills/oracle-testing/references/boundaries.md +579 -0
- package/skills/oracle-testing/references/core-testing.md +691 -0
- package/skills/oracle-testing/references/edge-testing.md +794 -0
- package/skills/oracle-testing/references/stub-driven-tdd.md +322 -0
- package/skills/oracle-thinkdeep/SKILL.md +164 -0
- package/skills/python-architecture/SKILL.md +148 -0
- package/skills/python-architecture/references/data-modeling.md +558 -0
- package/skills/python-architecture/references/ddd.md +569 -0
- package/skills/python-architecture/references/functional-core.md +337 -0
- package/skills/python-build-tools/SKILL.md +153 -0
- package/skills/python-build-tools/references/basedpyright.md +321 -0
- package/skills/python-build-tools/references/ruff.md +225 -0
- package/skills/python-build-tools/references/uv.md +257 -0
- package/skills/python-fastapi/SKILL.md +184 -0
- package/skills/python-fastapi/references/api-design.md +563 -0
- package/skills/python-fastapi/references/dependencies.md +479 -0
- package/skills/python-fastapi/references/middleware.md +475 -0
- package/skills/python-fastapi/references/validation.md +497 -0
- package/skills/python-modern-python/SKILL.md +201 -0
- package/skills/python-modern-python/references/async.md +76 -0
- package/skills/python-modern-python/references/pattern-matching.md +89 -0
- package/skills/python-modern-python/references/typing.md +96 -0
- package/skills/python-monorepo/SKILL.md +146 -0
- package/skills/python-monorepo/references/docker.md +515 -0
- package/skills/python-monorepo/references/namespace-packages.md +426 -0
- package/skills/python-monorepo/references/workspace-config.md +341 -0
- package/skills/python-sqlalchemy/SKILL.md +119 -0
- package/skills/python-sqlalchemy/references/async.md +51 -0
- package/skills/python-sqlalchemy/references/models.md +67 -0
- package/skills/python-sqlalchemy/references/queries.md +57 -0
- package/skills/python-temporal/SKILL.md +128 -0
- package/skills/python-temporal/references/patterns.md +87 -0
- package/skills/python-temporal/references/testing.md +54 -0
- package/skills/python-testing/SKILL.md +161 -0
- package/skills/python-testing/references/boundaries.md +449 -0
- package/skills/python-testing/references/mocking.md +449 -0
- package/skills/python-testing/references/pytest.md +524 -0
- package/skills/spec-brainstorm/SKILL.md +325 -0
- package/skills/spec-finish/SKILL.md +159 -0
- package/skills/spec-implement/SKILL.md +296 -0
- package/skills/spec-orchestrator/SKILL.md +156 -0
- package/skills/spec-plan/SKILL.md +248 -0
- package/skills/typescript-api-design/SKILL.md +396 -0
- package/skills/typescript-api-design/references/error-responses.md +469 -0
- package/skills/typescript-build-tools/SKILL.md +447 -0
- package/skills/typescript-build-tools/references/biome.md +436 -0
- package/skills/typescript-build-tools/references/bun.md +432 -0
- package/skills/typescript-build-tools/references/turborepo.md +426 -0
- package/skills/typescript-build-tools/references/vitest.md +518 -0
- package/skills/typescript-drizzle-orm/SKILL.md +398 -0
- package/skills/typescript-drizzle-orm/references/cloudflare.md +392 -0
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +412 -0
- package/skills/typescript-drizzle-orm/references/postgresql.md +191 -0
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +479 -0
- package/skills/typescript-drizzle-orm/references/sqlite.md +291 -0
- package/skills/typescript-dynamodb-toolbox/SKILL.md +370 -0
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +574 -0
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +495 -0
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +332 -0
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +631 -0
- package/skills/typescript-dynamodb-toolbox/references/testing.md +718 -0
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +423 -0
- package/skills/typescript-effect-ts/SKILL.md +341 -0
- package/skills/typescript-effect-ts/references/core.md +412 -0
- package/skills/typescript-effect-ts/references/error-handling.md +468 -0
- package/skills/typescript-effect-ts/references/resources.md +476 -0
- package/skills/typescript-effect-ts/references/schema.md +536 -0
- package/skills/typescript-effect-ts/references/services.md +461 -0
- package/skills/typescript-fastify/SKILL.md +405 -0
- package/skills/typescript-fastify/references/plugins.md +329 -0
- package/skills/typescript-fastify/references/typeid.md +315 -0
- package/skills/typescript-functional-patterns/SKILL.md +402 -0
- package/skills/typescript-functional-patterns/references/adts.md +497 -0
- package/skills/typescript-functional-patterns/references/branded-types.md +542 -0
- package/skills/typescript-functional-patterns/references/migration-guide.md +695 -0
- package/skills/typescript-functional-patterns/references/option-result.md +585 -0
- package/skills/typescript-testing/SKILL.md +353 -0
- package/skills/typescript-testing/references/mocking.md +675 -0
- package/skills/typescript-testing/references/msw.md +852 -0
- package/skills/typescript-testing/references/snapshot-testing.md +774 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
# Layer Boundary Testing
|
|
2
|
+
|
|
3
|
+
Test at component boundaries, not internals. Each layer has specific test focus areas.
|
|
4
|
+
|
|
5
|
+
## What to Test at Each Layer
|
|
6
|
+
|
|
7
|
+
### Entity Layer
|
|
8
|
+
- ✅ Domain logic and validation
|
|
9
|
+
- ✅ Transformations (from_request, to_response, to_record)
|
|
10
|
+
- ✅ Business rules and calculated fields
|
|
11
|
+
- ❌ Simple getters/setters
|
|
12
|
+
- ❌ Python language features
|
|
13
|
+
|
|
14
|
+
### Repository Layer
|
|
15
|
+
- ✅ CRUD operations
|
|
16
|
+
- ✅ Query logic and filters
|
|
17
|
+
- ✅ Entity ↔ Record transformations
|
|
18
|
+
- ✅ Database constraints
|
|
19
|
+
- ❌ ORM framework code
|
|
20
|
+
- ❌ SQL generation
|
|
21
|
+
|
|
22
|
+
### Service Layer
|
|
23
|
+
- ✅ Business workflows and orchestration
|
|
24
|
+
- ✅ Error handling and validation
|
|
25
|
+
- ✅ Dependency coordination
|
|
26
|
+
- ✅ Complex logic
|
|
27
|
+
- ❌ Trivial delegation
|
|
28
|
+
- ❌ Single-line pass-throughs
|
|
29
|
+
|
|
30
|
+
### Router Layer
|
|
31
|
+
- ✅ Request validation and deserialization
|
|
32
|
+
- ✅ Response serialization
|
|
33
|
+
- ✅ Status codes and HTTP semantics
|
|
34
|
+
- ✅ Error response formatting
|
|
35
|
+
- ❌ Framework routing mechanics
|
|
36
|
+
- ❌ Dependency injection
|
|
37
|
+
|
|
38
|
+
## Entity Layer Examples
|
|
39
|
+
|
|
40
|
+
### Test Transformations
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from entities import Product
|
|
44
|
+
from decimal import Decimal
|
|
45
|
+
from uuid import uuid4
|
|
46
|
+
|
|
47
|
+
def test_product_from_request():
|
|
48
|
+
"""Test creation from request object"""
|
|
49
|
+
request = CreateProductRequest(
|
|
50
|
+
name="Widget",
|
|
51
|
+
price=Decimal("9.99"),
|
|
52
|
+
sku="SKU123"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
product = Product.from_request(request)
|
|
56
|
+
|
|
57
|
+
assert product.name == "Widget"
|
|
58
|
+
assert product.price == Decimal("9.99")
|
|
59
|
+
assert product.sku == "SKU123"
|
|
60
|
+
assert isinstance(product.id, UUID)
|
|
61
|
+
assert product.created_at is not None
|
|
62
|
+
|
|
63
|
+
def test_product_to_response():
|
|
64
|
+
"""Test transformation to API response"""
|
|
65
|
+
product = Product(
|
|
66
|
+
id=uuid4(),
|
|
67
|
+
name="Widget",
|
|
68
|
+
price=Decimal("9.99"),
|
|
69
|
+
sku="SKU123",
|
|
70
|
+
created_at=datetime.now(),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
response = product.to_response()
|
|
74
|
+
|
|
75
|
+
assert response.id == str(product.id)
|
|
76
|
+
assert response.name == "Widget"
|
|
77
|
+
assert response.price == 9.99 # Decimal → float
|
|
78
|
+
|
|
79
|
+
def test_product_to_record():
|
|
80
|
+
"""Test transformation to database record"""
|
|
81
|
+
product = Product(
|
|
82
|
+
id=uuid4(),
|
|
83
|
+
name="Widget",
|
|
84
|
+
price=Decimal("9.99"),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
record = product.to_record()
|
|
88
|
+
|
|
89
|
+
assert record.id == product.id
|
|
90
|
+
assert record.name == "Widget"
|
|
91
|
+
assert record.price == Decimal("9.99")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Test Validation
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
def test_product_validate_price():
|
|
98
|
+
"""Test price validation"""
|
|
99
|
+
with pytest.raises(ValueError, match="Price must be positive"):
|
|
100
|
+
Product(
|
|
101
|
+
id=uuid4(),
|
|
102
|
+
name="Widget",
|
|
103
|
+
price=Decimal("-10"),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def test_product_validate_name():
|
|
107
|
+
"""Test name is required"""
|
|
108
|
+
with pytest.raises(ValueError, match="Name is required"):
|
|
109
|
+
Product(
|
|
110
|
+
id=uuid4(),
|
|
111
|
+
name="",
|
|
112
|
+
price=Decimal("9.99"),
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Test Business Logic
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
def test_product_apply_discount():
|
|
120
|
+
"""Test discount calculation"""
|
|
121
|
+
product = Product(
|
|
122
|
+
id=uuid4(),
|
|
123
|
+
name="Widget",
|
|
124
|
+
price=Decimal("100"),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
discounted = product.apply_discount(Decimal("0.1"))
|
|
128
|
+
|
|
129
|
+
assert discounted.price == Decimal("90")
|
|
130
|
+
assert product.price == Decimal("100") # Original unchanged
|
|
131
|
+
|
|
132
|
+
def test_product_tax_calculation():
|
|
133
|
+
"""Test tax on price"""
|
|
134
|
+
product = Product(
|
|
135
|
+
id=uuid4(),
|
|
136
|
+
name="Widget",
|
|
137
|
+
price=Decimal("100"),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
tax = product.calculate_tax(Decimal("0.08"))
|
|
141
|
+
|
|
142
|
+
assert tax == Decimal("8")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Service Layer Examples
|
|
146
|
+
|
|
147
|
+
### Test Business Workflows
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from unittest.mock import Mock, call
|
|
151
|
+
|
|
152
|
+
def test_create_product_service():
|
|
153
|
+
"""Test service orchestration"""
|
|
154
|
+
# Stub repository
|
|
155
|
+
mock_repo = Mock()
|
|
156
|
+
mock_repo.save.return_value = Product(
|
|
157
|
+
id=uuid4(),
|
|
158
|
+
name="Widget",
|
|
159
|
+
price=Decimal("9.99"),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Test service
|
|
163
|
+
service = ProductService(repo=mock_repo)
|
|
164
|
+
request = CreateProductRequest(name="Widget", price=Decimal("9.99"))
|
|
165
|
+
|
|
166
|
+
result = service.create(request)
|
|
167
|
+
|
|
168
|
+
# Verify repository was called
|
|
169
|
+
mock_repo.save.assert_called_once()
|
|
170
|
+
assert result.name == "Widget"
|
|
171
|
+
|
|
172
|
+
def test_apply_bulk_discount():
|
|
173
|
+
"""Test orchestration of complex business logic"""
|
|
174
|
+
mock_repo = Mock()
|
|
175
|
+
mock_repo.find_all.return_value = [
|
|
176
|
+
Product(id=uuid4(), name="A", price=Decimal("100")),
|
|
177
|
+
Product(id=uuid4(), name="B", price=Decimal("100")),
|
|
178
|
+
]
|
|
179
|
+
mock_repo.save.return_value = None
|
|
180
|
+
|
|
181
|
+
service = ProductService(repo=mock_repo)
|
|
182
|
+
service.apply_bulk_discount(Decimal("0.1"))
|
|
183
|
+
|
|
184
|
+
# Verify save called twice
|
|
185
|
+
assert mock_repo.save.call_count == 2
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Test Error Handling
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
def test_create_product_validation_error():
|
|
192
|
+
"""Test service error handling"""
|
|
193
|
+
mock_repo = Mock()
|
|
194
|
+
|
|
195
|
+
service = ProductService(repo=mock_repo)
|
|
196
|
+
|
|
197
|
+
with pytest.raises(ValueError, match="Invalid price"):
|
|
198
|
+
service.create(CreateProductRequest(
|
|
199
|
+
name="Widget",
|
|
200
|
+
price=Decimal("-10"),
|
|
201
|
+
))
|
|
202
|
+
|
|
203
|
+
# Repository not called on validation error
|
|
204
|
+
mock_repo.save.assert_not_called()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Repository Layer Examples
|
|
208
|
+
|
|
209
|
+
### Test Data Access
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
import pytest
|
|
213
|
+
from sqlalchemy import create_engine
|
|
214
|
+
from sqlalchemy.orm import sessionmaker
|
|
215
|
+
|
|
216
|
+
@pytest.fixture
|
|
217
|
+
def test_db():
|
|
218
|
+
"""In-memory test database"""
|
|
219
|
+
engine = create_engine("sqlite:///:memory:")
|
|
220
|
+
Base.metadata.create_all(engine)
|
|
221
|
+
SessionLocal = sessionmaker(bind=engine)
|
|
222
|
+
db = SessionLocal()
|
|
223
|
+
try:
|
|
224
|
+
yield db
|
|
225
|
+
finally:
|
|
226
|
+
db.close()
|
|
227
|
+
|
|
228
|
+
def test_repository_save(test_db):
|
|
229
|
+
"""Test saving entity"""
|
|
230
|
+
repo = ProductRepository(test_db)
|
|
231
|
+
|
|
232
|
+
product = Product(
|
|
233
|
+
id=uuid4(),
|
|
234
|
+
name="Widget",
|
|
235
|
+
price=Decimal("9.99"),
|
|
236
|
+
)
|
|
237
|
+
saved = repo.save(product)
|
|
238
|
+
|
|
239
|
+
assert saved.id == product.id
|
|
240
|
+
# Verify in database
|
|
241
|
+
record = test_db.query(ProductRecord).filter_by(id=product.id).first()
|
|
242
|
+
assert record is not None
|
|
243
|
+
assert record.name == "Widget"
|
|
244
|
+
|
|
245
|
+
def test_repository_find_by_id(test_db):
|
|
246
|
+
"""Test retrieving entity by ID"""
|
|
247
|
+
repo = ProductRepository(test_db)
|
|
248
|
+
product_id = uuid4()
|
|
249
|
+
|
|
250
|
+
# Create record
|
|
251
|
+
test_db.add(ProductRecord(
|
|
252
|
+
id=product_id,
|
|
253
|
+
name="Widget",
|
|
254
|
+
price=Decimal("9.99"),
|
|
255
|
+
))
|
|
256
|
+
test_db.commit()
|
|
257
|
+
|
|
258
|
+
# Find entity
|
|
259
|
+
found = repo.find_by_id(product_id)
|
|
260
|
+
|
|
261
|
+
assert found is not None
|
|
262
|
+
assert found.id == product_id
|
|
263
|
+
assert found.name == "Widget"
|
|
264
|
+
|
|
265
|
+
def test_repository_delete(test_db):
|
|
266
|
+
"""Test deleting entity"""
|
|
267
|
+
repo = ProductRepository(test_db)
|
|
268
|
+
product_id = uuid4()
|
|
269
|
+
|
|
270
|
+
# Create record
|
|
271
|
+
test_db.add(ProductRecord(id=product_id, name="Widget"))
|
|
272
|
+
test_db.commit()
|
|
273
|
+
|
|
274
|
+
# Delete
|
|
275
|
+
repo.delete(product_id)
|
|
276
|
+
|
|
277
|
+
# Verify deleted
|
|
278
|
+
assert test_db.query(ProductRecord).filter_by(id=product_id).first() is None
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Test Query Logic
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
def test_repository_find_by_name(test_db):
|
|
285
|
+
"""Test complex query"""
|
|
286
|
+
repo = ProductRepository(test_db)
|
|
287
|
+
|
|
288
|
+
# Insert test data
|
|
289
|
+
test_db.add_all([
|
|
290
|
+
ProductRecord(id=uuid4(), name="Widget A", price=Decimal("10")),
|
|
291
|
+
ProductRecord(id=uuid4(), name="Widget B", price=Decimal("20")),
|
|
292
|
+
ProductRecord(id=uuid4(), name="Gadget", price=Decimal("15")),
|
|
293
|
+
])
|
|
294
|
+
test_db.commit()
|
|
295
|
+
|
|
296
|
+
# Query
|
|
297
|
+
results = repo.find_by_name("Widget")
|
|
298
|
+
|
|
299
|
+
assert len(results) == 2
|
|
300
|
+
assert all("Widget" in p.name for p in results)
|
|
301
|
+
|
|
302
|
+
def test_repository_find_expensive_products(test_db):
|
|
303
|
+
"""Test filtering"""
|
|
304
|
+
repo = ProductRepository(test_db)
|
|
305
|
+
|
|
306
|
+
test_db.add_all([
|
|
307
|
+
ProductRecord(id=uuid4(), name="A", price=Decimal("10")),
|
|
308
|
+
ProductRecord(id=uuid4(), name="B", price=Decimal("100")),
|
|
309
|
+
ProductRecord(id=uuid4(), name="C", price=Decimal("200")),
|
|
310
|
+
])
|
|
311
|
+
test_db.commit()
|
|
312
|
+
|
|
313
|
+
results = repo.find_by_price_above(Decimal("50"))
|
|
314
|
+
|
|
315
|
+
assert len(results) == 2
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Router Layer Examples
|
|
319
|
+
|
|
320
|
+
### Test HTTP Endpoints
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
from fastapi.testclient import TestClient
|
|
324
|
+
|
|
325
|
+
def test_create_product_endpoint():
|
|
326
|
+
"""Test POST endpoint"""
|
|
327
|
+
client = TestClient(app)
|
|
328
|
+
|
|
329
|
+
response = client.post(
|
|
330
|
+
"/products",
|
|
331
|
+
json={"name": "Widget", "price": 9.99},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
assert response.status_code == 201
|
|
335
|
+
data = response.json()
|
|
336
|
+
assert data["name"] == "Widget"
|
|
337
|
+
assert data["price"] == 9.99
|
|
338
|
+
assert "id" in data
|
|
339
|
+
|
|
340
|
+
def test_get_product_endpoint(test_db):
|
|
341
|
+
"""Test GET endpoint"""
|
|
342
|
+
client = TestClient(app)
|
|
343
|
+
|
|
344
|
+
# Create product
|
|
345
|
+
create_response = client.post(
|
|
346
|
+
"/products",
|
|
347
|
+
json={"name": "Widget", "price": 9.99},
|
|
348
|
+
)
|
|
349
|
+
product_id = create_response.json()["id"]
|
|
350
|
+
|
|
351
|
+
# Get product
|
|
352
|
+
response = client.get(f"/products/{product_id}")
|
|
353
|
+
|
|
354
|
+
assert response.status_code == 200
|
|
355
|
+
assert response.json()["id"] == product_id
|
|
356
|
+
|
|
357
|
+
def test_update_product_endpoint():
|
|
358
|
+
"""Test PUT endpoint"""
|
|
359
|
+
client = TestClient(app)
|
|
360
|
+
|
|
361
|
+
# Create
|
|
362
|
+
create_resp = client.post(
|
|
363
|
+
"/products",
|
|
364
|
+
json={"name": "Widget", "price": 9.99},
|
|
365
|
+
)
|
|
366
|
+
product_id = create_resp.json()["id"]
|
|
367
|
+
|
|
368
|
+
# Update
|
|
369
|
+
response = client.put(
|
|
370
|
+
f"/products/{product_id}",
|
|
371
|
+
json={"name": "Updated Widget", "price": 19.99},
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
assert response.status_code == 200
|
|
375
|
+
assert response.json()["name"] == "Updated Widget"
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Test Error Responses
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
def test_get_nonexistent_product():
|
|
382
|
+
"""Test 404 response"""
|
|
383
|
+
client = TestClient(app)
|
|
384
|
+
|
|
385
|
+
response = client.get(f"/products/{uuid4()}")
|
|
386
|
+
|
|
387
|
+
assert response.status_code == 404
|
|
388
|
+
assert "not found" in response.json()["detail"].lower()
|
|
389
|
+
|
|
390
|
+
def test_invalid_request_body():
|
|
391
|
+
"""Test 422 validation error"""
|
|
392
|
+
client = TestClient(app)
|
|
393
|
+
|
|
394
|
+
response = client.post(
|
|
395
|
+
"/products",
|
|
396
|
+
json={"name": "Widget"}, # Missing price
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
assert response.status_code == 422
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Testing Anti-Patterns to Avoid
|
|
403
|
+
|
|
404
|
+
### Don't Test Everything
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
# BAD: Testing simple getters
|
|
408
|
+
def test_get_name():
|
|
409
|
+
product = Product(id=uuid4(), name="Widget")
|
|
410
|
+
assert product.name == "Widget" # Trivial
|
|
411
|
+
|
|
412
|
+
# GOOD: Test meaningful business logic
|
|
413
|
+
def test_apply_discount_calculates_correctly():
|
|
414
|
+
product = Product(id=uuid4(), price=Decimal("100"))
|
|
415
|
+
discounted = product.apply_discount(Decimal("0.1"))
|
|
416
|
+
assert discounted.price == Decimal("90")
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Don't Test Framework Code
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
# BAD: Testing SQLAlchemy internals
|
|
423
|
+
def test_sqlalchemy_session():
|
|
424
|
+
session = SessionLocal()
|
|
425
|
+
assert session is not None # Framework concern
|
|
426
|
+
|
|
427
|
+
# GOOD: Test your query logic
|
|
428
|
+
def test_find_by_price_returns_products():
|
|
429
|
+
repo = ProductRepository(test_db)
|
|
430
|
+
results = repo.find_by_price_above(Decimal("50"))
|
|
431
|
+
assert all(p.price > 50 for p in results)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Don't Over-Mock
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
# BAD: Mocking too much
|
|
438
|
+
def test_service():
|
|
439
|
+
mock_repo = Mock()
|
|
440
|
+
mock_entity = Mock()
|
|
441
|
+
mock_response = Mock()
|
|
442
|
+
# Multiple layers of mocks make test fragile
|
|
443
|
+
|
|
444
|
+
# GOOD: Real objects, stubbed boundaries
|
|
445
|
+
def test_service(test_db):
|
|
446
|
+
repo = ProductRepository(test_db) # Real
|
|
447
|
+
service = ProductService(repo) # Real
|
|
448
|
+
result = service.create(request) # One boundary tested
|
|
449
|
+
```
|