cfsa-antigravity 2.0.0 → 2.2.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/README.md +14 -0
- package/package.json +1 -1
- package/template/.agent/instructions/commands.md +8 -32
- package/template/.agent/instructions/example.md +21 -0
- package/template/.agent/instructions/patterns.md +3 -3
- package/template/.agent/instructions/tech-stack.md +71 -23
- package/template/.agent/instructions/workflow.md +12 -1
- package/template/.agent/rules/completion-checklist.md +6 -0
- package/template/.agent/rules/security-first.md +3 -3
- package/template/.agent/rules/vertical-slices.md +1 -1
- package/template/.agent/skill-library/MANIFEST.md +6 -0
- package/template/.agent/skill-library/stack/devops/git-advanced/SKILL.md +972 -0
- package/template/.agent/skill-library/stack/devops/git-workflow/SKILL.md +420 -0
- package/template/.agent/skills/api-versioning/SKILL.md +44 -298
- package/template/.agent/skills/api-versioning/references/typescript.md +157 -0
- package/template/.agent/skills/architecture-mapping/SKILL.md +13 -13
- package/template/.agent/skills/bootstrap-agents/SKILL.md +151 -152
- package/template/.agent/skills/clean-code/SKILL.md +64 -118
- package/template/.agent/skills/clean-code/references/typescript.md +126 -0
- package/template/.agent/skills/database-schema-design/SKILL.md +93 -317
- package/template/.agent/skills/database-schema-design/references/relational.md +228 -0
- package/template/.agent/skills/error-handling-patterns/SKILL.md +62 -557
- package/template/.agent/skills/error-handling-patterns/references/go.md +162 -0
- package/template/.agent/skills/error-handling-patterns/references/python.md +262 -0
- package/template/.agent/skills/error-handling-patterns/references/rust.md +112 -0
- package/template/.agent/skills/error-handling-patterns/references/typescript.md +178 -0
- package/template/.agent/skills/idea-extraction/SKILL.md +322 -224
- package/template/.agent/skills/logging-best-practices/SKILL.md +108 -767
- package/template/.agent/skills/logging-best-practices/references/go.md +49 -0
- package/template/.agent/skills/logging-best-practices/references/python.md +52 -0
- package/template/.agent/skills/logging-best-practices/references/typescript.md +215 -0
- package/template/.agent/skills/migration-management/SKILL.md +127 -311
- package/template/.agent/skills/migration-management/references/relational.md +214 -0
- package/template/.agent/skills/parallel-feature-development/SKILL.md +34 -43
- package/template/.agent/skills/pipeline-rubrics/references/be-rubric.md +1 -1
- package/template/.agent/skills/pipeline-rubrics/references/ia-rubric.md +2 -2
- package/template/.agent/skills/pipeline-rubrics/references/scoring.md +1 -1
- package/template/.agent/skills/pipeline-rubrics/references/vision-rubric.md +2 -1
- package/template/.agent/skills/prd-templates/SKILL.md +23 -6
- package/template/.agent/skills/prd-templates/references/be-spec-template.md +2 -2
- package/template/.agent/skills/prd-templates/references/decomposition-templates.md +2 -2
- package/template/.agent/skills/prd-templates/references/engineering-standards-template.md +2 -0
- package/template/.agent/skills/prd-templates/references/fe-spec-template.md +1 -1
- package/template/.agent/skills/prd-templates/references/fractal-cx-template.md +58 -0
- package/template/.agent/skills/prd-templates/references/fractal-feature-template.md +93 -0
- package/template/.agent/skills/prd-templates/references/fractal-node-index-template.md +55 -0
- package/template/.agent/skills/prd-templates/references/ideation-crosscut-template.md +26 -47
- package/template/.agent/skills/prd-templates/references/ideation-index-template.md +47 -31
- package/template/.agent/skills/prd-templates/references/operational-templates.md +1 -1
- package/template/.agent/skills/prd-templates/references/placeholder-workflow-mapping.md +50 -21
- package/template/.agent/skills/prd-templates/references/skill-loading-protocol.md +32 -0
- package/template/.agent/skills/prd-templates/references/slice-completion-gates.md +29 -0
- package/template/.agent/skills/prd-templates/references/spec-coverage-sweep.md +3 -3
- package/template/.agent/skills/prd-templates/references/tdd-testing-policy.md +39 -0
- package/template/.agent/skills/prd-templates/references/vision-template.md +8 -8
- package/template/.agent/skills/regex-patterns/SKILL.md +122 -540
- package/template/.agent/skills/regex-patterns/references/go.md +44 -0
- package/template/.agent/skills/regex-patterns/references/javascript.md +63 -0
- package/template/.agent/skills/regex-patterns/references/python.md +77 -0
- package/template/.agent/skills/regex-patterns/references/rust.md +43 -0
- package/template/.agent/skills/resolve-ambiguity/SKILL.md +1 -1
- package/template/.agent/skills/session-continuity/SKILL.md +11 -9
- package/template/.agent/skills/session-continuity/protocols/02-progress-generation.md +2 -2
- package/template/.agent/skills/session-continuity/protocols/04-pattern-extraction.md +1 -1
- package/template/.agent/skills/session-continuity/protocols/05-session-close.md +1 -1
- package/template/.agent/skills/session-continuity/protocols/09-parallel-claim.md +1 -1
- package/template/.agent/skills/session-continuity/protocols/10-placeholder-verification-gate.md +57 -78
- package/template/.agent/skills/session-continuity/protocols/11-parallel-synthesis.md +1 -1
- package/template/.agent/skills/spec-writing/SKILL.md +1 -1
- package/template/.agent/skills/tdd-workflow/SKILL.md +94 -317
- package/template/.agent/skills/tdd-workflow/references/typescript.md +231 -0
- package/template/.agent/skills/testing-strategist/SKILL.md +74 -687
- package/template/.agent/skills/testing-strategist/references/typescript.md +328 -0
- package/template/.agent/skills/workflow-automation/SKILL.md +62 -154
- package/template/.agent/skills/workflow-automation/references/inngest.md +88 -0
- package/template/.agent/skills/workflow-automation/references/temporal.md +64 -0
- package/template/.agent/workflows/bootstrap-agents-fill.md +85 -143
- package/template/.agent/workflows/bootstrap-agents-provision.md +90 -107
- package/template/.agent/workflows/create-prd-architecture.md +23 -16
- package/template/.agent/workflows/create-prd-compile.md +11 -12
- package/template/.agent/workflows/create-prd-design-system.md +1 -1
- package/template/.agent/workflows/create-prd-security.md +9 -11
- package/template/.agent/workflows/create-prd-stack.md +10 -4
- package/template/.agent/workflows/create-prd.md +9 -9
- package/template/.agent/workflows/decompose-architecture-structure.md +4 -6
- package/template/.agent/workflows/decompose-architecture-validate.md +18 -1
- package/template/.agent/workflows/decompose-architecture.md +18 -3
- package/template/.agent/workflows/evolve-contract.md +11 -11
- package/template/.agent/workflows/evolve-feature-classify.md +14 -6
- package/template/.agent/workflows/ideate-discover.md +72 -107
- package/template/.agent/workflows/ideate-extract.md +84 -63
- package/template/.agent/workflows/ideate-validate.md +26 -22
- package/template/.agent/workflows/ideate.md +9 -9
- package/template/.agent/workflows/implement-slice-setup.md +25 -23
- package/template/.agent/workflows/implement-slice-tdd.md +73 -89
- package/template/.agent/workflows/implement-slice.md +4 -4
- package/template/.agent/workflows/plan-phase-preflight.md +6 -2
- package/template/.agent/workflows/plan-phase-write.md +6 -8
- package/template/.agent/workflows/remediate-pipeline-assess.md +2 -1
- package/template/.agent/workflows/resolve-ambiguity.md +2 -2
- package/template/.agent/workflows/update-architecture-map.md +22 -5
- package/template/.agent/workflows/validate-phase-quality.md +155 -0
- package/template/.agent/workflows/validate-phase-readiness.md +167 -0
- package/template/.agent/workflows/validate-phase.md +19 -157
- package/template/.agent/workflows/verify-infrastructure.md +10 -10
- package/template/.agent/workflows/write-architecture-spec-design.md +23 -14
- package/template/.agent/workflows/write-be-spec-classify.md +25 -21
- package/template/.agent/workflows/write-be-spec.md +1 -1
- package/template/.agent/workflows/write-fe-spec-classify.md +6 -12
- package/template/.agent/workflows/write-fe-spec-write.md +1 -1
- package/template/AGENTS.md +6 -2
- package/template/GEMINI.md +5 -3
- package/template/docs/README.md +10 -10
- package/template/docs/kit-architecture.md +126 -33
- package/template/docs/plans/ideation/README.md +8 -3
- package/template/.agent/skills/prd-templates/references/ideation-domain-template.md +0 -55
|
@@ -6,7 +6,18 @@ version: 1.0.0
|
|
|
6
6
|
|
|
7
7
|
# Testing Strategist
|
|
8
8
|
|
|
9
|
-
Test the right things at the right level
|
|
9
|
+
Test the right things at the right level — write tests that give you confidence to ship.
|
|
10
|
+
|
|
11
|
+
## Stack-Specific References
|
|
12
|
+
|
|
13
|
+
After reading the methodology below, read the reference matching your surface's Languages column in the surface stack map (`.agent/instructions/tech-stack.md`):
|
|
14
|
+
|
|
15
|
+
| Language | Reference |
|
|
16
|
+
|----------|-----------|
|
|
17
|
+
| TypeScript / JavaScript | `references/typescript.md` |
|
|
18
|
+
| Python | `references/python.md` |
|
|
19
|
+
| Go | `references/go.md` |
|
|
20
|
+
| Rust | `references/rust.md` |
|
|
10
21
|
|
|
11
22
|
## Core Principle
|
|
12
23
|
|
|
@@ -14,11 +25,11 @@ Test the right things at the right level - write tests that give you confidence
|
|
|
14
25
|
|
|
15
26
|
Tests should be:
|
|
16
27
|
|
|
17
|
-
- **Fast**
|
|
18
|
-
- **Isolated**
|
|
19
|
-
- **Repeatable**
|
|
20
|
-
- **Self-checking**
|
|
21
|
-
- **Timely**
|
|
28
|
+
- **Fast** — Run in milliseconds (unit) to seconds (integration) to minutes (E2E)
|
|
29
|
+
- **Isolated** — Test one thing at a time
|
|
30
|
+
- **Repeatable** — Same input = same output
|
|
31
|
+
- **Self-checking** — Pass/fail automatically, no manual verification
|
|
32
|
+
- **Timely** — Written alongside code (or before, with TDD)
|
|
22
33
|
|
|
23
34
|
---
|
|
24
35
|
|
|
@@ -57,194 +68,24 @@ Test individual functions, components, or classes in isolation.
|
|
|
57
68
|
**Good candidates:**
|
|
58
69
|
|
|
59
70
|
- ✅ Business logic functions (calculations, validation, transformations)
|
|
60
|
-
- ✅ Utility functions (
|
|
61
|
-
- ✅
|
|
62
|
-
- ✅ Hooks (custom React hooks)
|
|
71
|
+
- ✅ Utility functions (formatting, parsing, etc.)
|
|
72
|
+
- ✅ UI components (rendering, props, state)
|
|
63
73
|
- ✅ Pure functions (same input = same output)
|
|
64
74
|
|
|
65
75
|
**Skip:**
|
|
66
76
|
|
|
67
77
|
- ❌ Third-party libraries (assume they work)
|
|
68
|
-
- ❌ Framework internals
|
|
78
|
+
- ❌ Framework internals
|
|
69
79
|
- ❌ Simple getters/setters with no logic
|
|
70
80
|
|
|
71
|
-
### Unit Test Examples
|
|
72
|
-
|
|
73
|
-
#### Testing Business Logic (Jest + TypeScript)
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
// src/lib/pricing.ts
|
|
77
|
-
export function calculateTotal(items: { price: number; quantity: number }[]) {
|
|
78
|
-
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function applyDiscount(total: number, discountPercent: number) {
|
|
82
|
-
if (discountPercent < 0 || discountPercent > 100) {
|
|
83
|
-
throw new Error('Invalid discount percentage')
|
|
84
|
-
}
|
|
85
|
-
return total * (1 - discountPercent / 100)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// src/lib/pricing.test.ts
|
|
89
|
-
import { calculateTotal, applyDiscount } from './pricing'
|
|
90
|
-
|
|
91
|
-
describe('calculateTotal', () => {
|
|
92
|
-
it('calculates total for single item', () => {
|
|
93
|
-
const items = [{ price: 10, quantity: 2 }]
|
|
94
|
-
expect(calculateTotal(items)).toBe(20)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('calculates total for multiple items', () => {
|
|
98
|
-
const items = [
|
|
99
|
-
{ price: 10, quantity: 2 },
|
|
100
|
-
{ price: 5, quantity: 3 }
|
|
101
|
-
]
|
|
102
|
-
expect(calculateTotal(items)).toBe(35)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('returns 0 for empty array', () => {
|
|
106
|
-
expect(calculateTotal([])).toBe(0)
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
describe('applyDiscount', () => {
|
|
111
|
-
it('applies discount correctly', () => {
|
|
112
|
-
expect(applyDiscount(100, 20)).toBe(80)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('throws error for invalid discount', () => {
|
|
116
|
-
expect(() => applyDiscount(100, -10)).toThrow('Invalid discount')
|
|
117
|
-
expect(() => applyDiscount(100, 150)).toThrow('Invalid discount')
|
|
118
|
-
})
|
|
119
|
-
})
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
#### Testing React Components (Jest + React Testing Library)
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
// src/components/Button.tsx
|
|
126
|
-
export function Button({
|
|
127
|
-
children,
|
|
128
|
-
variant = 'primary',
|
|
129
|
-
onClick
|
|
130
|
-
}: {
|
|
131
|
-
children: React.ReactNode
|
|
132
|
-
variant?: 'primary' | 'secondary'
|
|
133
|
-
onClick?: () => void
|
|
134
|
-
}) {
|
|
135
|
-
return (
|
|
136
|
-
<button
|
|
137
|
-
className={`btn btn-${variant}`}
|
|
138
|
-
onClick={onClick}
|
|
139
|
-
>
|
|
140
|
-
{children}
|
|
141
|
-
</button>
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// src/components/Button.test.tsx
|
|
146
|
-
import { render, screen, fireEvent } from '@testing-library/react'
|
|
147
|
-
import { Button } from './Button'
|
|
148
|
-
|
|
149
|
-
describe('Button', () => {
|
|
150
|
-
it('renders with correct text', () => {
|
|
151
|
-
render(<Button>Click me</Button>)
|
|
152
|
-
expect(screen.getByText('Click me')).toBeInTheDocument()
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('applies primary variant by default', () => {
|
|
156
|
-
render(<Button>Click me</Button>)
|
|
157
|
-
expect(screen.getByRole('button')).toHaveClass('btn-primary')
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('applies secondary variant when specified', () => {
|
|
161
|
-
render(<Button variant="secondary">Click me</Button>)
|
|
162
|
-
expect(screen.getByRole('button')).toHaveClass('btn-secondary')
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('calls onClick when clicked', () => {
|
|
166
|
-
const handleClick = jest.fn()
|
|
167
|
-
render(<Button onClick={handleClick}>Click me</Button>)
|
|
168
|
-
|
|
169
|
-
fireEvent.click(screen.getByRole('button'))
|
|
170
|
-
expect(handleClick).toHaveBeenCalledTimes(1)
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
#### Testing Custom Hooks
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
// src/hooks/useCounter.ts
|
|
179
|
-
import { useState } from 'react'
|
|
180
|
-
|
|
181
|
-
export function useCounter(initialValue = 0) {
|
|
182
|
-
const [count, setCount] = useState(initialValue)
|
|
183
|
-
|
|
184
|
-
const increment = () => setCount(c => c + 1)
|
|
185
|
-
const decrement = () => setCount(c => c - 1)
|
|
186
|
-
const reset = () => setCount(initialValue)
|
|
187
|
-
|
|
188
|
-
return { count, increment, decrement, reset }
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// src/hooks/useCounter.test.ts
|
|
192
|
-
import { renderHook, act } from '@testing-library/react'
|
|
193
|
-
import { useCounter } from './useCounter'
|
|
194
|
-
|
|
195
|
-
describe('useCounter', () => {
|
|
196
|
-
it('initializes with default value', () => {
|
|
197
|
-
const { result } = renderHook(() => useCounter())
|
|
198
|
-
expect(result.current.count).toBe(0)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('initializes with custom value', () => {
|
|
202
|
-
const { result } = renderHook(() => useCounter(10))
|
|
203
|
-
expect(result.current.count).toBe(10)
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('increments count', () => {
|
|
207
|
-
const { result } = renderHook(() => useCounter())
|
|
208
|
-
|
|
209
|
-
act(() => {
|
|
210
|
-
result.current.increment()
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
expect(result.current.count).toBe(1)
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
it('decrements count', () => {
|
|
217
|
-
const { result } = renderHook(() => useCounter(5))
|
|
218
|
-
|
|
219
|
-
act(() => {
|
|
220
|
-
result.current.decrement()
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
expect(result.current.count).toBe(4)
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
it('resets to initial value', () => {
|
|
227
|
-
const { result } = renderHook(() => useCounter(10))
|
|
228
|
-
|
|
229
|
-
act(() => {
|
|
230
|
-
result.current.increment()
|
|
231
|
-
result.current.increment()
|
|
232
|
-
result.current.reset()
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
expect(result.current.count).toBe(10)
|
|
236
|
-
})
|
|
237
|
-
})
|
|
238
|
-
```
|
|
239
|
-
|
|
240
81
|
### Unit Test Best Practices
|
|
241
82
|
|
|
242
83
|
✅ **Do:**
|
|
243
84
|
|
|
244
85
|
- Test behavior, not implementation
|
|
245
|
-
- Use descriptive test names
|
|
86
|
+
- Use descriptive test names
|
|
246
87
|
- Follow AAA pattern: Arrange, Act, Assert
|
|
247
|
-
- Test edge cases (empty
|
|
88
|
+
- Test edge cases (empty, null, negative, boundary values)
|
|
248
89
|
- Keep tests simple and readable
|
|
249
90
|
|
|
250
91
|
❌ **Don't:**
|
|
@@ -260,184 +101,22 @@ describe('useCounter', () => {
|
|
|
260
101
|
|
|
261
102
|
### What to Test
|
|
262
103
|
|
|
263
|
-
Test multiple units working together
|
|
104
|
+
Test multiple units working together — typically API routes, database operations, or service integrations.
|
|
264
105
|
|
|
265
106
|
**Good candidates:**
|
|
266
107
|
|
|
267
|
-
- ✅ API endpoints (request →
|
|
108
|
+
- ✅ API endpoints (request → handler → database → response)
|
|
268
109
|
- ✅ Database operations (queries, transactions)
|
|
269
|
-
- ✅ Third-party integrations (
|
|
110
|
+
- ✅ Third-party integrations (payment, email, etc.)
|
|
270
111
|
- ✅ Authentication flows
|
|
271
112
|
- ✅ File upload/download
|
|
272
113
|
|
|
273
|
-
### Integration Test Examples
|
|
274
|
-
|
|
275
|
-
#### Testing API Routes (Next.js + Supertest)
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
// app/api/posts/route.ts
|
|
279
|
-
export async function GET() {
|
|
280
|
-
const posts = await db.post.findMany({
|
|
281
|
-
include: { author: true },
|
|
282
|
-
orderBy: { createdAt: 'desc' }
|
|
283
|
-
})
|
|
284
|
-
return Response.json(posts)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export async function POST(request: Request) {
|
|
288
|
-
const body = await request.json()
|
|
289
|
-
|
|
290
|
-
// Validate
|
|
291
|
-
const result = PostSchema.safeParse(body)
|
|
292
|
-
if (!result.success) {
|
|
293
|
-
return Response.json({ errors: result.error.issues }, { status: 400 })
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Create post
|
|
297
|
-
const post = await db.post.create({
|
|
298
|
-
data: {
|
|
299
|
-
title: result.data.title,
|
|
300
|
-
content: result.data.content,
|
|
301
|
-
authorId: request.user.id
|
|
302
|
-
}
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
return Response.json(post, { status: 201 })
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// app/api/posts/route.test.ts
|
|
309
|
-
import { testClient } from '@/lib/test-utils'
|
|
310
|
-
|
|
311
|
-
describe('POST /api/posts', () => {
|
|
312
|
-
beforeEach(async () => {
|
|
313
|
-
// Clean database before each test
|
|
314
|
-
await db.post.deleteMany()
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
it('creates a new post', async () => {
|
|
318
|
-
const response = await testClient
|
|
319
|
-
.post('/api/posts')
|
|
320
|
-
.set('Authorization', `Bearer ${authToken}`)
|
|
321
|
-
.send({
|
|
322
|
-
title: 'Test Post',
|
|
323
|
-
content: 'This is a test post'
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
expect(response.status).toBe(201)
|
|
327
|
-
expect(response.body).toMatchObject({
|
|
328
|
-
title: 'Test Post',
|
|
329
|
-
content: 'This is a test post'
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
// Verify in database
|
|
333
|
-
const posts = await db.post.findMany()
|
|
334
|
-
expect(posts).toHaveLength(1)
|
|
335
|
-
expect(posts[0].title).toBe('Test Post')
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
it('returns 400 for invalid data', async () => {
|
|
339
|
-
const response = await testClient
|
|
340
|
-
.post('/api/posts')
|
|
341
|
-
.set('Authorization', `Bearer ${authToken}`)
|
|
342
|
-
.send({
|
|
343
|
-
title: '' // Invalid: empty title
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
expect(response.status).toBe(400)
|
|
347
|
-
expect(response.body.errors).toBeDefined()
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
it('returns 401 for unauthenticated request', async () => {
|
|
351
|
-
const response = await testClient.post('/api/posts').send({
|
|
352
|
-
title: 'Test',
|
|
353
|
-
content: 'Test'
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
expect(response.status).toBe(401)
|
|
357
|
-
})
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
describe('GET /api/posts', () => {
|
|
361
|
-
beforeEach(async () => {
|
|
362
|
-
await db.post.deleteMany()
|
|
363
|
-
|
|
364
|
-
// Seed test data
|
|
365
|
-
await db.post.createMany({
|
|
366
|
-
data: [
|
|
367
|
-
{ title: 'Post 1', content: 'Content 1', authorId: user.id },
|
|
368
|
-
{ title: 'Post 2', content: 'Content 2', authorId: user.id }
|
|
369
|
-
]
|
|
370
|
-
})
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
it('returns all posts', async () => {
|
|
374
|
-
const response = await testClient.get('/api/posts')
|
|
375
|
-
|
|
376
|
-
expect(response.status).toBe(200)
|
|
377
|
-
expect(response.body).toHaveLength(2)
|
|
378
|
-
expect(response.body[0].author).toBeDefined()
|
|
379
|
-
})
|
|
380
|
-
})
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
#### Testing Database Operations
|
|
384
|
-
|
|
385
|
-
```typescript
|
|
386
|
-
// src/lib/repositories/userRepository.test.ts
|
|
387
|
-
import { db } from '@/lib/db'
|
|
388
|
-
import { createUser, findUserByEmail, updateUser } from './userRepository'
|
|
389
|
-
|
|
390
|
-
describe('userRepository', () => {
|
|
391
|
-
beforeEach(async () => {
|
|
392
|
-
await db.user.deleteMany()
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
afterAll(async () => {
|
|
396
|
-
await db.$disconnect()
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
describe('createUser', () => {
|
|
400
|
-
it('creates user with hashed password', async () => {
|
|
401
|
-
const user = await createUser({
|
|
402
|
-
email: 'test@example.com',
|
|
403
|
-
password: 'password123'
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
expect(user.email).toBe('test@example.com')
|
|
407
|
-
expect(user.password).not.toBe('password123') // Should be hashed
|
|
408
|
-
expect(user.password).toMatch(/^\$2[aby]/) // bcrypt hash format
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
it('throws error for duplicate email', async () => {
|
|
412
|
-
await createUser({ email: 'test@example.com', password: 'pass' })
|
|
413
|
-
|
|
414
|
-
await expect(createUser({ email: 'test@example.com', password: 'pass' })).rejects.toThrow()
|
|
415
|
-
})
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
describe('findUserByEmail', () => {
|
|
419
|
-
it('finds existing user', async () => {
|
|
420
|
-
await createUser({ email: 'test@example.com', password: 'pass' })
|
|
421
|
-
|
|
422
|
-
const user = await findUserByEmail('test@example.com')
|
|
423
|
-
expect(user).toBeDefined()
|
|
424
|
-
expect(user?.email).toBe('test@example.com')
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
it('returns null for non-existent user', async () => {
|
|
428
|
-
const user = await findUserByEmail('nonexistent@example.com')
|
|
429
|
-
expect(user).toBeNull()
|
|
430
|
-
})
|
|
431
|
-
})
|
|
432
|
-
})
|
|
433
|
-
```
|
|
434
|
-
|
|
435
114
|
### Integration Test Best Practices
|
|
436
115
|
|
|
437
116
|
✅ **Do:**
|
|
438
117
|
|
|
439
118
|
- Use test database (separate from development/production)
|
|
440
|
-
- Clean up test data
|
|
119
|
+
- Clean up test data before/after each test
|
|
441
120
|
- Test happy path + error cases
|
|
442
121
|
- Test authentication/authorization
|
|
443
122
|
- Use factories/fixtures for test data
|
|
@@ -468,113 +147,20 @@ Test complete user journeys through the actual UI.
|
|
|
468
147
|
- ❌ Every possible UI interaction (too slow/brittle)
|
|
469
148
|
- ❌ Edge cases (cover with unit/integration tests)
|
|
470
149
|
|
|
471
|
-
### E2E Test Examples (Playwright)
|
|
472
|
-
|
|
473
|
-
```typescript
|
|
474
|
-
// tests/e2e/auth.spec.ts
|
|
475
|
-
import { test, expect } from '@playwright/test'
|
|
476
|
-
|
|
477
|
-
test.describe('Authentication', () => {
|
|
478
|
-
test('user can sign up and log in', async ({ page }) => {
|
|
479
|
-
// Navigate to signup
|
|
480
|
-
await page.goto('/signup')
|
|
481
|
-
|
|
482
|
-
// Fill signup form
|
|
483
|
-
await page.fill('input[name="email"]', 'test@example.com')
|
|
484
|
-
await page.fill('input[name="password"]', 'SecurePass123!')
|
|
485
|
-
await page.fill('input[name="confirmPassword"]', 'SecurePass123!')
|
|
486
|
-
|
|
487
|
-
// Submit form
|
|
488
|
-
await page.click('button[type="submit"]')
|
|
489
|
-
|
|
490
|
-
// Should redirect to dashboard
|
|
491
|
-
await expect(page).toHaveURL('/dashboard')
|
|
492
|
-
await expect(page.locator('h1')).toContainText('Welcome')
|
|
493
|
-
|
|
494
|
-
// Logout
|
|
495
|
-
await page.click('[data-testid="logout-button"]')
|
|
496
|
-
|
|
497
|
-
// Should redirect to login
|
|
498
|
-
await expect(page).toHaveURL('/login')
|
|
499
|
-
|
|
500
|
-
// Login again
|
|
501
|
-
await page.fill('input[name="email"]', 'test@example.com')
|
|
502
|
-
await page.fill('input[name="password"]', 'SecurePass123!')
|
|
503
|
-
await page.click('button[type="submit"]')
|
|
504
|
-
|
|
505
|
-
// Should be back at dashboard
|
|
506
|
-
await expect(page).toHaveURL('/dashboard')
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
test('shows error for invalid credentials', async ({ page }) => {
|
|
510
|
-
await page.goto('/login')
|
|
511
|
-
|
|
512
|
-
await page.fill('input[name="email"]', 'wrong@example.com')
|
|
513
|
-
await page.fill('input[name="password"]', 'wrongpassword')
|
|
514
|
-
await page.click('button[type="submit"]')
|
|
515
|
-
|
|
516
|
-
// Should show error message
|
|
517
|
-
await expect(page.locator('[role="alert"]')).toContainText('Invalid credentials')
|
|
518
|
-
|
|
519
|
-
// Should stay on login page
|
|
520
|
-
await expect(page).toHaveURL('/login')
|
|
521
|
-
})
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
// tests/e2e/checkout.spec.ts
|
|
525
|
-
test.describe('Checkout Flow', () => {
|
|
526
|
-
test('user can complete purchase', async ({ page }) => {
|
|
527
|
-
// Login first
|
|
528
|
-
await page.goto('/login')
|
|
529
|
-
await page.fill('input[name="email"]', 'test@example.com')
|
|
530
|
-
await page.fill('input[name="password"]', 'password')
|
|
531
|
-
await page.click('button[type="submit"]')
|
|
532
|
-
|
|
533
|
-
// Add product to cart
|
|
534
|
-
await page.goto('/products')
|
|
535
|
-
await page.click('[data-testid="product-1"] button:text("Add to Cart")')
|
|
536
|
-
|
|
537
|
-
// Verify cart badge
|
|
538
|
-
await expect(page.locator('[data-testid="cart-badge"]')).toContainText('1')
|
|
539
|
-
|
|
540
|
-
// Go to checkout
|
|
541
|
-
await page.click('[data-testid="cart-button"]')
|
|
542
|
-
await page.click('button:text("Checkout")')
|
|
543
|
-
|
|
544
|
-
// Fill shipping info
|
|
545
|
-
await page.fill('input[name="address"]', '123 Main St')
|
|
546
|
-
await page.fill('input[name="city"]', 'San Francisco')
|
|
547
|
-
await page.fill('input[name="zip"]', '94103')
|
|
548
|
-
|
|
549
|
-
// Fill payment info (test mode)
|
|
550
|
-
await page.fill('input[name="cardNumber"]', '4242424242424242')
|
|
551
|
-
await page.fill('input[name="expiry"]', '12/25')
|
|
552
|
-
await page.fill('input[name="cvc"]', '123')
|
|
553
|
-
|
|
554
|
-
// Submit order
|
|
555
|
-
await page.click('button:text("Place Order")')
|
|
556
|
-
|
|
557
|
-
// Should see confirmation
|
|
558
|
-
await expect(page).toHaveURL(/\/orders\/\d+/)
|
|
559
|
-
await expect(page.locator('h1')).toContainText('Order Confirmed')
|
|
560
|
-
})
|
|
561
|
-
})
|
|
562
|
-
```
|
|
563
|
-
|
|
564
150
|
### E2E Test Best Practices
|
|
565
151
|
|
|
566
152
|
✅ **Do:**
|
|
567
153
|
|
|
568
154
|
- Test critical paths only (< 20 tests)
|
|
569
|
-
- Use
|
|
155
|
+
- Use stable selectors (test IDs, roles, labels — not CSS classes)
|
|
570
156
|
- Run in CI/CD pipeline
|
|
571
|
-
- Test across browsers
|
|
157
|
+
- Test across browsers if web surface
|
|
572
158
|
- Take screenshots on failure
|
|
573
159
|
|
|
574
160
|
❌ **Don't:**
|
|
575
161
|
|
|
576
162
|
- Test every UI variation
|
|
577
|
-
- Use fragile selectors (
|
|
163
|
+
- Use fragile selectors (nth-child, CSS classes)
|
|
578
164
|
- Run E2E tests on every commit (too slow)
|
|
579
165
|
- Ignore flaky tests (fix or remove them)
|
|
580
166
|
|
|
@@ -584,51 +170,10 @@ test.describe('Checkout Flow', () => {
|
|
|
584
170
|
|
|
585
171
|
### The Red-Green-Refactor Cycle
|
|
586
172
|
|
|
587
|
-
1. **Red:** Write a failing test
|
|
173
|
+
1. **Red:** Write a failing test that defines expected behavior
|
|
588
174
|
2. **Green:** Write minimal code to make it pass
|
|
589
175
|
3. **Refactor:** Improve code while keeping tests green
|
|
590
176
|
|
|
591
|
-
### TDD Example
|
|
592
|
-
|
|
593
|
-
```typescript
|
|
594
|
-
// 1. RED: Write failing test first
|
|
595
|
-
describe('formatCurrency', () => {
|
|
596
|
-
it('formats number as USD currency', () => {
|
|
597
|
-
expect(formatCurrency(1234.56)).toBe('$1,234.56')
|
|
598
|
-
})
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
// Run test: FAILS (formatCurrency doesn't exist)
|
|
602
|
-
|
|
603
|
-
// 2. GREEN: Write minimal implementation
|
|
604
|
-
export function formatCurrency(amount: number): string {
|
|
605
|
-
return `$${amount.toLocaleString('en-US', { minimumFractionDigits: 2 })}`
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Run test: PASSES
|
|
609
|
-
|
|
610
|
-
// 3. REFACTOR: Improve implementation
|
|
611
|
-
export function formatCurrency(
|
|
612
|
-
amount: number,
|
|
613
|
-
currency: string = 'USD',
|
|
614
|
-
locale: string = 'en-US'
|
|
615
|
-
): string {
|
|
616
|
-
return new Intl.NumberFormat(locale, {
|
|
617
|
-
style: 'currency',
|
|
618
|
-
currency
|
|
619
|
-
}).format(amount)
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Add more tests
|
|
623
|
-
it('formats EUR currency', () => {
|
|
624
|
-
expect(formatCurrency(1234.56, 'EUR', 'de-DE')).toBe('1.234,56 €')
|
|
625
|
-
})
|
|
626
|
-
|
|
627
|
-
it('handles negative amounts', () => {
|
|
628
|
-
expect(formatCurrency(-100)).toBe('-$100.00')
|
|
629
|
-
})
|
|
630
|
-
```
|
|
631
|
-
|
|
632
177
|
### When to Use TDD
|
|
633
178
|
|
|
634
179
|
**Good for:**
|
|
@@ -653,67 +198,46 @@ it('handles negative amounts', () => {
|
|
|
653
198
|
- ✅ External APIs (slow, unreliable, cost money)
|
|
654
199
|
- ✅ Time/randomness (make tests deterministic)
|
|
655
200
|
- ✅ File system operations
|
|
656
|
-
- ✅ Database (in unit tests only)
|
|
657
|
-
|
|
658
|
-
### Mock Examples (Jest)
|
|
201
|
+
- ✅ Database (in unit tests only — NEVER in integration tests)
|
|
659
202
|
|
|
660
|
-
|
|
661
|
-
// Mock external API
|
|
662
|
-
import { fetchUserData } from '@/lib/api'
|
|
203
|
+
### Anti-Mock-Abuse Rules
|
|
663
204
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
mockFetchUserData.mockResolvedValue({
|
|
669
|
-
id: '1',
|
|
670
|
-
name: 'John Doe',
|
|
671
|
-
email: 'john@example.com'
|
|
672
|
-
})
|
|
673
|
-
|
|
674
|
-
render(<UserProfile userId="1" />)
|
|
675
|
-
|
|
676
|
-
await waitFor(() => {
|
|
677
|
-
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
|
678
|
-
})
|
|
679
|
-
})
|
|
680
|
-
|
|
681
|
-
// Mock Date
|
|
682
|
-
beforeAll(() => {
|
|
683
|
-
jest.useFakeTimers()
|
|
684
|
-
jest.setSystemTime(new Date('2024-01-01'))
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
afterAll(() => {
|
|
688
|
-
jest.useRealTimers()
|
|
689
|
-
})
|
|
690
|
-
|
|
691
|
-
it('shows correct date', () => {
|
|
692
|
-
expect(getCurrentDate()).toBe('2024-01-01')
|
|
693
|
-
})
|
|
694
|
-
|
|
695
|
-
// Mock Math.random
|
|
696
|
-
const mockRandom = jest.spyOn(Math, 'random')
|
|
697
|
-
mockRandom.mockReturnValue(0.5)
|
|
698
|
-
|
|
699
|
-
expect(generateRandomId()).toBe('expected-id-with-0.5-random')
|
|
700
|
-
|
|
701
|
-
mockRandom.mockRestore()
|
|
702
|
-
```
|
|
205
|
+
1. **Never mock what you own** — if you wrote the module, test it directly
|
|
206
|
+
2. **One mock = one boundary** — only mock at the boundary between your code and external systems
|
|
207
|
+
3. **Verify mock interactions** — assert that mocked deps were called with correct arguments
|
|
208
|
+
4. **Mock return values, not internals** — mock the return value, not the internal implementation
|
|
703
209
|
|
|
704
210
|
### Mocking Best Practices
|
|
705
211
|
|
|
706
212
|
✅ **Do:**
|
|
707
213
|
|
|
708
|
-
- Mock at boundaries (APIs, file system)
|
|
214
|
+
- Mock at boundaries (APIs, file system, network)
|
|
709
215
|
- Restore mocks after tests
|
|
710
216
|
- Make mocks realistic (same shape as real data)
|
|
711
217
|
|
|
712
218
|
❌ **Don't:**
|
|
713
219
|
|
|
714
|
-
- Over-mock (makes tests brittle)
|
|
220
|
+
- Over-mock (makes tests brittle and meaningless)
|
|
715
221
|
- Mock your own code (test real behavior)
|
|
716
|
-
-
|
|
222
|
+
- Leave mocks unreset between tests
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Assertion Depth Rules
|
|
227
|
+
|
|
228
|
+
### ❌ Shallow Assertions (BANNED)
|
|
229
|
+
|
|
230
|
+
Assertions that prove nothing about correctness:
|
|
231
|
+
- "Result is defined" — undefined would also be defined
|
|
232
|
+
- "Result is truthy" — empty arrays and objects are truthy
|
|
233
|
+
- "Status is 200" alone — doesn't verify response body
|
|
234
|
+
|
|
235
|
+
### ✅ Deep Assertions (REQUIRED)
|
|
236
|
+
|
|
237
|
+
Every test must assert:
|
|
238
|
+
1. **The correct output** — specific values, not just types
|
|
239
|
+
2. **The correct side effects** — what changed in the system
|
|
240
|
+
3. **The correct error behavior** — specific error type and message
|
|
717
241
|
|
|
718
242
|
---
|
|
719
243
|
|
|
@@ -721,20 +245,20 @@ mockRandom.mockRestore()
|
|
|
721
245
|
|
|
722
246
|
### Coverage Targets
|
|
723
247
|
|
|
724
|
-
- **70% minimum**
|
|
725
|
-
- **80% good**
|
|
726
|
-
- **90%+ diminishing returns**
|
|
248
|
+
- **70% minimum** — Below this, you're missing important tests
|
|
249
|
+
- **80% good** — Solid coverage of critical paths
|
|
250
|
+
- **90%+ diminishing returns** — Chasing 100% often not worth it
|
|
727
251
|
|
|
728
252
|
### What to Focus On
|
|
729
253
|
|
|
730
|
-
**High priority (
|
|
254
|
+
**High priority (90%+ coverage):**
|
|
731
255
|
|
|
732
256
|
- Business logic
|
|
733
257
|
- Authentication/authorization
|
|
734
258
|
- Payment processing
|
|
735
259
|
- Data validation
|
|
736
260
|
|
|
737
|
-
**Medium priority (
|
|
261
|
+
**Medium priority (70%+):**
|
|
738
262
|
|
|
739
263
|
- API routes
|
|
740
264
|
- Database queries
|
|
@@ -742,128 +266,22 @@ mockRandom.mockRestore()
|
|
|
742
266
|
|
|
743
267
|
**Low priority (okay to skip):**
|
|
744
268
|
|
|
745
|
-
- UI components (test behavior, not
|
|
269
|
+
- UI components (test behavior, not rendering)
|
|
746
270
|
- Configuration files
|
|
747
271
|
- Type definitions
|
|
748
|
-
- Third-party integrations (integration tests better)
|
|
749
|
-
|
|
750
|
-
### Checking Coverage
|
|
751
|
-
|
|
752
|
-
```bash
|
|
753
|
-
# Jest
|
|
754
|
-
npm test -- --coverage
|
|
755
|
-
|
|
756
|
-
# View HTML report
|
|
757
|
-
open coverage/lcov-report/index.html
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
### Coverage Configuration (jest.config.js)
|
|
761
|
-
|
|
762
|
-
```javascript
|
|
763
|
-
module.exports = {
|
|
764
|
-
collectCoverageFrom: [
|
|
765
|
-
'src/**/*.{ts,tsx}',
|
|
766
|
-
'!src/**/*.d.ts',
|
|
767
|
-
'!src/**/*.stories.tsx',
|
|
768
|
-
'!src/types/**'
|
|
769
|
-
],
|
|
770
|
-
coverageThresholds: {
|
|
771
|
-
global: {
|
|
772
|
-
branches: 70,
|
|
773
|
-
functions: 70,
|
|
774
|
-
lines: 70,
|
|
775
|
-
statements: 70
|
|
776
|
-
},
|
|
777
|
-
// Critical paths need higher coverage
|
|
778
|
-
'./src/lib/auth/**': {
|
|
779
|
-
branches: 90,
|
|
780
|
-
functions: 90,
|
|
781
|
-
lines: 90
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
```
|
|
786
|
-
|
|
787
|
-
---
|
|
788
|
-
|
|
789
|
-
## Testing Strategies by Framework
|
|
790
|
-
|
|
791
|
-
### Next.js (React)
|
|
792
|
-
|
|
793
|
-
- Unit: Jest + React Testing Library
|
|
794
|
-
- Integration: Supertest (API routes)
|
|
795
|
-
- E2E: Playwright
|
|
796
|
-
|
|
797
|
-
### Express API
|
|
798
|
-
|
|
799
|
-
- Unit: Jest
|
|
800
|
-
- Integration: Supertest
|
|
801
|
-
- E2E: Playwright (if has UI)
|
|
802
|
-
|
|
803
|
-
### FastAPI (Python)
|
|
804
|
-
|
|
805
|
-
- Unit: pytest
|
|
806
|
-
- Integration: pytest + TestClient
|
|
807
|
-
- E2E: Playwright
|
|
808
272
|
|
|
809
273
|
---
|
|
810
274
|
|
|
811
275
|
## Common Testing Patterns
|
|
812
276
|
|
|
813
277
|
### Testing Async Code
|
|
814
|
-
|
|
815
|
-
```typescript
|
|
816
|
-
// Using async/await
|
|
817
|
-
it('fetches user data', async () => {
|
|
818
|
-
const user = await fetchUser('123')
|
|
819
|
-
expect(user.name).toBe('John')
|
|
820
|
-
})
|
|
821
|
-
|
|
822
|
-
// Using waitFor (React Testing Library)
|
|
823
|
-
it('shows loading then data', async () => {
|
|
824
|
-
render(<UserProfile userId="123" />)
|
|
825
|
-
|
|
826
|
-
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
|
827
|
-
|
|
828
|
-
await waitFor(() => {
|
|
829
|
-
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
|
830
|
-
})
|
|
831
|
-
})
|
|
832
|
-
```
|
|
278
|
+
Write tests that properly wait for async operations to complete before asserting. Use your framework's async test utilities.
|
|
833
279
|
|
|
834
280
|
### Testing Error Handling
|
|
835
|
-
|
|
836
|
-
```typescript
|
|
837
|
-
it('handles errors gracefully', async () => {
|
|
838
|
-
mockFetchUser.mockRejectedValue(new Error('Network error'))
|
|
839
|
-
|
|
840
|
-
render(<UserProfile userId="123" />)
|
|
841
|
-
|
|
842
|
-
await waitFor(() => {
|
|
843
|
-
expect(screen.getByText(/error/i)).toBeInTheDocument()
|
|
844
|
-
})
|
|
845
|
-
})
|
|
846
|
-
```
|
|
281
|
+
Mock dependencies to throw errors, then assert your code handles them correctly — shows the right error message, returns the right status code, logs appropriately.
|
|
847
282
|
|
|
848
283
|
### Testing Forms
|
|
849
|
-
|
|
850
|
-
```typescript
|
|
851
|
-
it('submits form with valid data', async () => {
|
|
852
|
-
const handleSubmit = jest.fn()
|
|
853
|
-
render(<LoginForm onSubmit={handleSubmit} />)
|
|
854
|
-
|
|
855
|
-
await userEvent.type(screen.getByLabelText('Email'), 'test@example.com')
|
|
856
|
-
await userEvent.type(screen.getByLabelText('Password'), 'password123')
|
|
857
|
-
await userEvent.click(screen.getByRole('button', { name: 'Login' }))
|
|
858
|
-
|
|
859
|
-
await waitFor(() => {
|
|
860
|
-
expect(handleSubmit).toHaveBeenCalledWith({
|
|
861
|
-
email: 'test@example.com',
|
|
862
|
-
password: 'password123'
|
|
863
|
-
})
|
|
864
|
-
})
|
|
865
|
-
})
|
|
866
|
-
```
|
|
284
|
+
Fill form fields programmatically, submit, and assert both the submission data and any validation errors.
|
|
867
285
|
|
|
868
286
|
---
|
|
869
287
|
|
|
@@ -871,33 +289,24 @@ it('submits form with valid data', async () => {
|
|
|
871
289
|
|
|
872
290
|
### File Structure
|
|
873
291
|
|
|
292
|
+
Tests co-locate with source files:
|
|
293
|
+
|
|
874
294
|
```
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
│ └── Button.test.tsx # Co-located with component
|
|
879
|
-
├── lib/
|
|
880
|
-
│ ├── utils.ts
|
|
881
|
-
│ └── utils.test.ts # Co-located with module
|
|
882
|
-
└── __tests__/
|
|
883
|
-
├── integration/ # Integration tests
|
|
884
|
-
│ └── api.test.ts
|
|
885
|
-
└── e2e/ # E2E tests
|
|
886
|
-
└── checkout.spec.ts
|
|
295
|
+
source-file.ext → source-file.test.ext (unit tests)
|
|
296
|
+
route-handler.ext → route-handler.test.ext (integration tests)
|
|
297
|
+
e2e/ → feature-name.e2e.ext (E2E tests)
|
|
887
298
|
```
|
|
888
299
|
|
|
889
300
|
### Naming Conventions
|
|
890
301
|
|
|
891
|
-
- Unit/Integration: `*.test
|
|
892
|
-
- E2E: `*.e2e
|
|
893
|
-
- Test names
|
|
302
|
+
- Unit/Integration: `*.test.*` or `*.spec.*`
|
|
303
|
+
- E2E: `*.e2e.*` or `*.spec.*` (in tests/e2e/)
|
|
304
|
+
- Test names should describe behavior: "returns X when Y" or "throws error for invalid input"
|
|
894
305
|
|
|
895
306
|
---
|
|
896
307
|
|
|
897
308
|
## When to Use This Skill
|
|
898
309
|
|
|
899
|
-
Use testing-strategist skill when:
|
|
900
|
-
|
|
901
310
|
- ✅ Setting up testing for new project
|
|
902
311
|
- ✅ Choosing test frameworks
|
|
903
312
|
- ✅ Deciding what to test and at what level
|
|
@@ -907,26 +316,4 @@ Use testing-strategist skill when:
|
|
|
907
316
|
|
|
908
317
|
---
|
|
909
318
|
|
|
910
|
-
## Related Resources
|
|
911
|
-
|
|
912
|
-
**Skills:**
|
|
913
|
-
|
|
914
|
-
- `security-engineer` - Security testing
|
|
915
|
-
- `api-designer` - API testing strategies
|
|
916
|
-
- `frontend-builder` - React testing patterns
|
|
917
|
-
|
|
918
|
-
**Patterns:**
|
|
919
|
-
|
|
920
|
-
- `/STANDARDS/best-practices/testing-best-practices.md`
|
|
921
|
-
- `/TEMPLATES/testing/jest-nextjs-setup.md`
|
|
922
|
-
- `/TEMPLATES/testing/playwright-e2e-setup.md`
|
|
923
|
-
|
|
924
|
-
**External:**
|
|
925
|
-
|
|
926
|
-
- [Jest Documentation](https://jestjs.io/)
|
|
927
|
-
- [React Testing Library](https://testing-library.com/react)
|
|
928
|
-
- [Playwright](https://playwright.dev/)
|
|
929
|
-
|
|
930
|
-
---
|
|
931
|
-
|
|
932
319
|
**Good tests give you confidence to ship.** ✅
|