codecruise 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/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/codecruise.js +68 -0
- package/config/CLAUDE.md +107 -0
- package/config/agents/analyst.md +48 -0
- package/config/agents/architect-reviewer.md +161 -0
- package/config/agents/architect.md +119 -0
- package/config/agents/critic.md +63 -0
- package/config/agents/developer.md +96 -0
- package/config/agents/devops.md +81 -0
- package/config/agents/orchestrator.md +91 -0
- package/config/agents/planner.md +139 -0
- package/config/agents/retro.md +52 -0
- package/config/agents/reviewer.md +101 -0
- package/config/agents/security-reviewer.md +57 -0
- package/config/agents/stack/expo/AGENT.md +473 -0
- package/config/agents/stack/expo/rules/critical.md +427 -0
- package/config/agents/stack/expo/rules/native.md +455 -0
- package/config/agents/stack/expo/rules/navigation.md +445 -0
- package/config/agents/stack/expo/rules/performance.md +415 -0
- package/config/agents/stack/fastify/AGENT.md +397 -0
- package/config/agents/stack/fastify/rules/api-design.md +283 -0
- package/config/agents/stack/fastify/rules/critical.md +232 -0
- package/config/agents/stack/fastify/rules/queues.md +303 -0
- package/config/agents/stack/fastify/rules/security.md +384 -0
- package/config/agents/stack/index.yaml +48 -0
- package/config/agents/stack/nextjs/AGENT.md +421 -0
- package/config/agents/stack/nextjs/rules/components.md +413 -0
- package/config/agents/stack/nextjs/rules/critical.md +391 -0
- package/config/agents/stack/nextjs/rules/performance.md +403 -0
- package/config/agents/stack/nextjs/rules/styling.md +334 -0
- package/config/agents/stack/shared-ts/AGENT.md +384 -0
- package/config/agents/stack/shared-ts/rules/critical.md +315 -0
- package/config/agents/stack/shared-ts/rules/patterns.md +384 -0
- package/config/agents/stack/shared-ts/rules/zod.md +427 -0
- package/config/agents/tester.md +79 -0
- package/config/commands/architect-discuss.md +366 -0
- package/config/commands/architect-list.md +160 -0
- package/config/commands/architect-review.md +111 -0
- package/config/commands/architect.md +118 -0
- package/config/commands/compact.md +118 -0
- package/config/commands/companion.md +279 -0
- package/config/commands/dashboard.md +152 -0
- package/config/commands/doctor.md +227 -0
- package/config/commands/dogfood-report.md +101 -0
- package/config/commands/flags/run-autonomous.md +110 -0
- package/config/commands/flags/run-pause.md +80 -0
- package/config/commands/ingest.md +173 -0
- package/config/commands/init.md +128 -0
- package/config/commands/metrics.md +87 -0
- package/config/commands/parallel.md +320 -0
- package/config/commands/pause.md +55 -0
- package/config/commands/plan-review.md +130 -0
- package/config/commands/plan.md +216 -0
- package/config/commands/production-check.md +308 -0
- package/config/commands/refine.md +323 -0
- package/config/commands/resume.md +72 -0
- package/config/commands/retro.md +121 -0
- package/config/commands/retry.md +75 -0
- package/config/commands/role.md +310 -0
- package/config/commands/run.md +417 -0
- package/config/commands/scope.md +85 -0
- package/config/commands/setup-permissions.md +104 -0
- package/config/commands/skip.md +75 -0
- package/config/commands/spec-forge.md +213 -0
- package/config/commands/spec-help.md +194 -0
- package/config/commands/spec-patch.md +342 -0
- package/config/commands/spec-resolve.md +110 -0
- package/config/commands/spec-review.md +153 -0
- package/config/commands/status.md +114 -0
- package/config/commands/sync.md +131 -0
- package/config/commands/task.md +138 -0
- package/config/commands/verify.md +124 -0
- package/config/hooks/README.md +632 -0
- package/config/hooks/activity-log.sh +187 -0
- package/config/hooks/anti-rationalize.sh +52 -0
- package/config/hooks/capture-verification.sh +112 -0
- package/config/hooks/collect-metrics.sh +135 -0
- package/config/hooks/enforce-file-scope.sh +75 -0
- package/config/hooks/enforce-state-machine.sh +161 -0
- package/config/hooks/enforce-tdd.sh +180 -0
- package/config/hooks/format.sh +40 -0
- package/config/hooks/lib/activity-helpers.sh +162 -0
- package/config/hooks/lib/read-settings.sh +71 -0
- package/config/hooks/load-context-skills.sh +95 -0
- package/config/hooks/notify.sh +81 -0
- package/config/hooks/pre-commit.sample +35 -0
- package/config/hooks/protect-files.sh +63 -0
- package/config/hooks/track-agents.sh +41 -0
- package/config/hooks/track-commands.sh +37 -0
- package/config/hooks/track-enforcement.sh +44 -0
- package/config/hooks/track-ooda.sh +77 -0
- package/config/hooks/validate-commit-msg.sh +35 -0
- package/config/hooks/validate-plan.sh +213 -0
- package/config/hooks/verify-criteria.sh +46 -0
- package/config/hooks/verify-todo-completion.sh +140 -0
- package/config/rules/comments.md +25 -0
- package/config/rules/decision-rules.md +308 -0
- package/config/rules/hygiene.md +247 -0
- package/config/rules/pattern-detection.md +372 -0
- package/config/rules/profiles.md +193 -0
- package/config/rules/recovery.md +83 -0
- package/config/rules/scope-detection.md +213 -0
- package/config/rules/standards.md +127 -0
- package/config/rules/workflow.md +121 -0
- package/config/schemas.md +767 -0
- package/config/settings.json +195 -0
- package/config/skills/backend/SKILL.md +734 -0
- package/config/skills/database/SKILL.md +426 -0
- package/config/skills/frontend/SKILL.md +434 -0
- package/config/skills/git/SKILL.md +396 -0
- package/config/skills/index.yaml +36 -0
- package/config/skills/observability/SKILL.md +430 -0
- package/config/skills/package-dev/SKILL.md +498 -0
- package/config/skills/performance/SKILL.md +378 -0
- package/config/skills/resilience/SKILL.md +573 -0
- package/config/skills/testing/SKILL.md +398 -0
- package/config/skills/testing-patterns/SKILL.md +276 -0
- package/config/skills/typescript/SKILL.md +152 -0
- package/config/templates/CLAUDE.md +70 -0
- package/config/templates/README.md +117 -0
- package/config/templates/steering/adr-template.md +102 -0
- package/config/templates/steering/product.md +60 -0
- package/config/templates/steering/rfc-template.md +159 -0
- package/config/templates/steering/structure.md +146 -0
- package/config/templates/steering/tech.md +85 -0
- package/package.json +40 -0
- package/src/install.js +163 -0
- package/src/report.js +310 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing
|
|
3
|
+
description: TDD workflow, unit, integration, and E2E testing
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
triggers:
|
|
6
|
+
- test
|
|
7
|
+
- TDD
|
|
8
|
+
- vitest
|
|
9
|
+
- jest
|
|
10
|
+
- playwright
|
|
11
|
+
- spec
|
|
12
|
+
- mock
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Testing Skill
|
|
16
|
+
|
|
17
|
+
TDD workflow and comprehensive testing patterns.
|
|
18
|
+
|
|
19
|
+
## Quick Reference
|
|
20
|
+
|
|
21
|
+
### TDD Workflow
|
|
22
|
+
|
|
23
|
+
| ID | Rule | Priority |
|
|
24
|
+
|----|------|----------|
|
|
25
|
+
| tdd-1 | Write failing test BEFORE implementation | CRITICAL |
|
|
26
|
+
| tdd-2 | Write minimal code to pass the test | CRITICAL |
|
|
27
|
+
| tdd-3 | Refactor only after tests pass | CRITICAL |
|
|
28
|
+
| tdd-4 | One logical assertion per test | HIGH |
|
|
29
|
+
| tdd-5 | Test behavior, not implementation | HIGH |
|
|
30
|
+
| tdd-6 | Name tests as behavior descriptions | HIGH |
|
|
31
|
+
| tdd-7 | Use Arrange-Act-Assert pattern | HIGH |
|
|
32
|
+
| tdd-8 | Keep tests deterministic (no randomness) | CRITICAL |
|
|
33
|
+
|
|
34
|
+
### Unit Testing
|
|
35
|
+
|
|
36
|
+
| ID | Rule | Priority |
|
|
37
|
+
|----|------|----------|
|
|
38
|
+
| unit-1 | Test pure functions first | HIGH |
|
|
39
|
+
| unit-2 | Mock external dependencies | HIGH |
|
|
40
|
+
| unit-3 | Use test.each for parameterized tests | MEDIUM |
|
|
41
|
+
| unit-4 | Test edge cases explicitly | HIGH |
|
|
42
|
+
| unit-5 | Test error conditions | HIGH |
|
|
43
|
+
| unit-6 | Keep tests under 20 lines | MEDIUM |
|
|
44
|
+
| unit-7 | No logic in tests (no if/loops) | HIGH |
|
|
45
|
+
|
|
46
|
+
### Component Testing
|
|
47
|
+
|
|
48
|
+
| ID | Rule | Priority |
|
|
49
|
+
|----|------|----------|
|
|
50
|
+
| comp-1 | Query by role, label, or text | CRITICAL |
|
|
51
|
+
| comp-2 | Use userEvent, not fireEvent | HIGH |
|
|
52
|
+
| comp-3 | Test user interactions, not state | HIGH |
|
|
53
|
+
| comp-4 | Test accessibility (roles, labels) | HIGH |
|
|
54
|
+
| comp-5 | Test loading and error states | HIGH |
|
|
55
|
+
| comp-6 | Avoid snapshot tests | MEDIUM |
|
|
56
|
+
| comp-7 | Use findBy for async content | HIGH |
|
|
57
|
+
|
|
58
|
+
### E2E Testing
|
|
59
|
+
|
|
60
|
+
| ID | Rule | Priority |
|
|
61
|
+
|----|------|----------|
|
|
62
|
+
| e2e-1 | Test critical user journeys | HIGH |
|
|
63
|
+
| e2e-2 | Use Page Object Model | HIGH |
|
|
64
|
+
| e2e-3 | Keep E2E tests minimal and focused | HIGH |
|
|
65
|
+
| e2e-4 | Run E2E in CI with retries | MEDIUM |
|
|
66
|
+
| e2e-5 | Use stable selectors (data-testid) | MEDIUM |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## TDD Cycle
|
|
71
|
+
|
|
72
|
+
### tdd-1, tdd-2, tdd-3: RED → GREEN → REFACTOR
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// 1. RED: Write failing test
|
|
76
|
+
describe('formatCurrency', () => {
|
|
77
|
+
it('should format positive numbers with $ symbol', () => {
|
|
78
|
+
expect(formatCurrency(1234.56)).toBe('$1,234.56');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Run test → FAILS (function doesn't exist)
|
|
83
|
+
|
|
84
|
+
// 2. GREEN: Minimal implementation
|
|
85
|
+
function formatCurrency(amount: number): string {
|
|
86
|
+
return `$${amount.toLocaleString('en-US', {
|
|
87
|
+
minimumFractionDigits: 2
|
|
88
|
+
})}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Run test → PASSES
|
|
92
|
+
|
|
93
|
+
// 3. REFACTOR: Clean up (tests still pass)
|
|
94
|
+
const formatter = new Intl.NumberFormat('en-US', {
|
|
95
|
+
style: 'currency',
|
|
96
|
+
currency: 'USD',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function formatCurrency(amount: number): string {
|
|
100
|
+
return formatter.format(amount);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Run test → STILL PASSES
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### tdd-7: Arrange-Act-Assert
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
it('should create user with hashed password', async () => {
|
|
110
|
+
// Arrange
|
|
111
|
+
const input = { email: 'test@example.com', password: 'secret123' };
|
|
112
|
+
|
|
113
|
+
// Act
|
|
114
|
+
const user = await createUser(input);
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
expect(user.email).toBe('test@example.com');
|
|
118
|
+
expect(user.password).not.toBe('secret123');
|
|
119
|
+
expect(user.password).toHaveLength(60); // bcrypt hash
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Unit Testing
|
|
126
|
+
|
|
127
|
+
### unit-3: Parameterized Tests
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
describe('validateEmail', () => {
|
|
131
|
+
it.each([
|
|
132
|
+
['user@example.com', true],
|
|
133
|
+
['user+tag@example.com', true],
|
|
134
|
+
['invalid', false],
|
|
135
|
+
['@example.com', false],
|
|
136
|
+
['user@', false],
|
|
137
|
+
])('validateEmail(%s) should return %s', (email, expected) => {
|
|
138
|
+
expect(validateEmail(email)).toBe(expected);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Mocking
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Mock module
|
|
147
|
+
vi.mock('@/lib/database', () => ({
|
|
148
|
+
db: {
|
|
149
|
+
user: {
|
|
150
|
+
findUnique: vi.fn(),
|
|
151
|
+
create: vi.fn(),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
import { db } from '@/lib/database';
|
|
157
|
+
|
|
158
|
+
describe('getUser', () => {
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
vi.clearAllMocks();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should return user from database', async () => {
|
|
164
|
+
const mockUser = { id: '1', name: 'John' };
|
|
165
|
+
vi.mocked(db.user.findUnique).mockResolvedValue(mockUser);
|
|
166
|
+
|
|
167
|
+
const user = await getUser('1');
|
|
168
|
+
|
|
169
|
+
expect(user).toEqual(mockUser);
|
|
170
|
+
expect(db.user.findUnique).toHaveBeenCalledWith({
|
|
171
|
+
where: { id: '1' },
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Async Testing
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
it('should throw on invalid id', async () => {
|
|
181
|
+
await expect(fetchUser('invalid')).rejects.toThrow('User not found');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should resolve with user data', async () => {
|
|
185
|
+
const user = await fetchUser('123');
|
|
186
|
+
expect(user).toMatchObject({
|
|
187
|
+
id: '123',
|
|
188
|
+
name: expect.any(String),
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Component Testing
|
|
196
|
+
|
|
197
|
+
### comp-1: Query by Role/Label
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// GOOD: Accessible queries
|
|
201
|
+
screen.getByRole('button', { name: /submit/i });
|
|
202
|
+
screen.getByLabelText(/email/i);
|
|
203
|
+
screen.getByText(/welcome/i);
|
|
204
|
+
|
|
205
|
+
// BAD: Implementation details
|
|
206
|
+
screen.getByTestId('submit-btn'); // Only as last resort
|
|
207
|
+
screen.getByClassName('btn-primary'); // Never
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### comp-2: userEvent Over fireEvent
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import userEvent from '@testing-library/user-event';
|
|
214
|
+
|
|
215
|
+
it('should submit form on enter', async () => {
|
|
216
|
+
const user = userEvent.setup();
|
|
217
|
+
const onSubmit = vi.fn();
|
|
218
|
+
|
|
219
|
+
render(<SearchForm onSubmit={onSubmit} />);
|
|
220
|
+
|
|
221
|
+
await user.type(screen.getByRole('textbox'), 'search query');
|
|
222
|
+
await user.keyboard('{Enter}');
|
|
223
|
+
|
|
224
|
+
expect(onSubmit).toHaveBeenCalledWith('search query');
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### comp-5: Loading and Error States
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
it('should show loading state', () => {
|
|
232
|
+
render(<UserProfile isLoading />);
|
|
233
|
+
|
|
234
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
|
235
|
+
expect(screen.queryByText(/john/i)).not.toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should show error state', () => {
|
|
239
|
+
render(<UserProfile error="Failed to load" />);
|
|
240
|
+
|
|
241
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Failed to load');
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### comp-7: Async Content
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
it('should load and display user', async () => {
|
|
249
|
+
render(<UserProfile userId="123" />);
|
|
250
|
+
|
|
251
|
+
// findBy waits for element
|
|
252
|
+
const name = await screen.findByText(/john doe/i);
|
|
253
|
+
expect(name).toBeInTheDocument();
|
|
254
|
+
|
|
255
|
+
// Loading should be gone
|
|
256
|
+
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Testing Hooks
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import { renderHook, act } from '@testing-library/react';
|
|
264
|
+
|
|
265
|
+
describe('useCounter', () => {
|
|
266
|
+
it('should increment count', () => {
|
|
267
|
+
const { result } = renderHook(() => useCounter());
|
|
268
|
+
|
|
269
|
+
act(() => {
|
|
270
|
+
result.current.increment();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(result.current.count).toBe(1);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## E2E Testing (Playwright)
|
|
281
|
+
|
|
282
|
+
### e2e-2: Page Object Model
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// e2e/pages/login.page.ts
|
|
286
|
+
export class LoginPage {
|
|
287
|
+
constructor(private page: Page) {}
|
|
288
|
+
|
|
289
|
+
readonly emailInput = () => this.page.getByLabel('Email');
|
|
290
|
+
readonly passwordInput = () => this.page.getByLabel('Password');
|
|
291
|
+
readonly submitButton = () => this.page.getByRole('button', { name: 'Sign in' });
|
|
292
|
+
readonly errorMessage = () => this.page.getByRole('alert');
|
|
293
|
+
|
|
294
|
+
async goto() {
|
|
295
|
+
await this.page.goto('/login');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async login(email: string, password: string) {
|
|
299
|
+
await this.emailInput().fill(email);
|
|
300
|
+
await this.passwordInput().fill(password);
|
|
301
|
+
await this.submitButton().click();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### E2E Test
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { test, expect } from '@playwright/test';
|
|
310
|
+
import { LoginPage } from './pages/login.page';
|
|
311
|
+
|
|
312
|
+
test('should login successfully', async ({ page }) => {
|
|
313
|
+
const loginPage = new LoginPage(page);
|
|
314
|
+
|
|
315
|
+
await loginPage.goto();
|
|
316
|
+
await loginPage.login('user@example.com', 'password123');
|
|
317
|
+
|
|
318
|
+
await expect(page).toHaveURL('/dashboard');
|
|
319
|
+
await expect(page.getByText('Welcome back')).toBeVisible();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('should show error with invalid credentials', async ({ page }) => {
|
|
323
|
+
const loginPage = new LoginPage(page);
|
|
324
|
+
|
|
325
|
+
await loginPage.goto();
|
|
326
|
+
await loginPage.login('user@example.com', 'wrong');
|
|
327
|
+
|
|
328
|
+
await expect(loginPage.errorMessage()).toContainText('Invalid credentials');
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Test Data Factories
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { faker } from '@faker-js/faker';
|
|
338
|
+
|
|
339
|
+
export function createUser(overrides: Partial<User> = {}): User {
|
|
340
|
+
return {
|
|
341
|
+
id: faker.string.uuid(),
|
|
342
|
+
email: faker.internet.email(),
|
|
343
|
+
name: faker.person.fullName(),
|
|
344
|
+
role: 'user',
|
|
345
|
+
createdAt: faker.date.past(),
|
|
346
|
+
...overrides,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Usage
|
|
351
|
+
const users = Array.from({ length: 5 }, () => createUser());
|
|
352
|
+
const admin = createUser({ role: 'admin' });
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Configuration
|
|
358
|
+
|
|
359
|
+
### Vitest
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// vitest.config.ts
|
|
363
|
+
export default defineConfig({
|
|
364
|
+
test: {
|
|
365
|
+
environment: 'jsdom',
|
|
366
|
+
globals: true,
|
|
367
|
+
setupFiles: ['./tests/setup.ts'],
|
|
368
|
+
coverage: {
|
|
369
|
+
provider: 'v8',
|
|
370
|
+
reporter: ['text', 'html'],
|
|
371
|
+
exclude: ['node_modules/', '**/*.d.ts'],
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Test Setup
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// tests/setup.ts
|
|
381
|
+
import '@testing-library/jest-dom/vitest';
|
|
382
|
+
import { cleanup } from '@testing-library/react';
|
|
383
|
+
import { afterEach } from 'vitest';
|
|
384
|
+
|
|
385
|
+
afterEach(() => {
|
|
386
|
+
cleanup();
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Coverage Goals
|
|
393
|
+
|
|
394
|
+
| Type | Target | Focus |
|
|
395
|
+
|------|--------|-------|
|
|
396
|
+
| Unit | 80%+ | Utils, hooks, pure functions |
|
|
397
|
+
| Integration | 70%+ | Components, API routes |
|
|
398
|
+
| E2E | Critical paths | Auth, checkout, core flows |
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-patterns
|
|
3
|
+
description: Testing pyramid, mocking, coverage, TDD, integration tests, E2E
|
|
4
|
+
keywords: [testing, vitest, playwright, mock, coverage, tdd, integration, e2e, test pyramid]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Testing Standards
|
|
8
|
+
|
|
9
|
+
Testing patterns and requirements for production-grade software.
|
|
10
|
+
|
|
11
|
+
## Test Pyramid
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
╱╲ E2E (5%) - Critical flows only
|
|
15
|
+
╱──╲ Contract (10%) - API schemas
|
|
16
|
+
╱────╲ Integration (25%) - Real services
|
|
17
|
+
╱──────╲ Unit (60%) - Business logic
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- Never skip unit tests for integration
|
|
21
|
+
- E2E for critical money/auth flows only
|
|
22
|
+
- Integration tests catch real DB/cache bugs
|
|
23
|
+
|
|
24
|
+
## Coverage Requirements
|
|
25
|
+
|
|
26
|
+
| Layer | Minimum | Target |
|
|
27
|
+
|-------|---------|--------|
|
|
28
|
+
| Infrastructure packages | 85% | 95% |
|
|
29
|
+
| Business logic packages | 70% | 85% |
|
|
30
|
+
| Applications (unit) | 60% | 80% |
|
|
31
|
+
|
|
32
|
+
Block merge on coverage drop >2%.
|
|
33
|
+
|
|
34
|
+
## When to Write Tests
|
|
35
|
+
|
|
36
|
+
### TDD (Test First)
|
|
37
|
+
- Business logic services
|
|
38
|
+
- Data transformations
|
|
39
|
+
- Validation rules
|
|
40
|
+
- Error handling
|
|
41
|
+
|
|
42
|
+
### Test After
|
|
43
|
+
- UI components
|
|
44
|
+
- Styling changes
|
|
45
|
+
- Configuration
|
|
46
|
+
|
|
47
|
+
### Always Test
|
|
48
|
+
- Happy path
|
|
49
|
+
- 2+ error cases per function
|
|
50
|
+
- Edge cases (null, empty, max, boundary)
|
|
51
|
+
- Security-sensitive code (auth, payments)
|
|
52
|
+
|
|
53
|
+
## Mock Strategy
|
|
54
|
+
|
|
55
|
+
| Test Type | Database | Redis | External APIs |
|
|
56
|
+
|-----------|----------|-------|---------------|
|
|
57
|
+
| Unit | Mock | Mock | Mock |
|
|
58
|
+
| Integration | Real (Testcontainers) | Real | Mock |
|
|
59
|
+
| E2E | Real (staging) | Real | Real or sandbox |
|
|
60
|
+
|
|
61
|
+
### Mock Utilities
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Database mocking (@figu/db pattern)
|
|
65
|
+
import { createMockRawDrizzleDb, mockSelectChain } from '@figu/db/test-utils';
|
|
66
|
+
|
|
67
|
+
const { mockDb, mockQuery } = createMockRawDrizzleDb();
|
|
68
|
+
mockSelectChain(mockQuery, [{ id: 'user_123' }]);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Real Services (Testcontainers)
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// Integration tests with real PostgreSQL
|
|
75
|
+
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
|
76
|
+
|
|
77
|
+
let container: StartedPostgreSqlContainer;
|
|
78
|
+
|
|
79
|
+
beforeAll(async () => {
|
|
80
|
+
container = await new PostgreSqlContainer().start();
|
|
81
|
+
process.env.DATABASE_URL = container.getConnectionUri();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterAll(async () => {
|
|
85
|
+
await container.stop();
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Test Structure
|
|
90
|
+
|
|
91
|
+
### File Organization
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
src/
|
|
95
|
+
├── service.ts
|
|
96
|
+
├── service.test.ts # Unit tests (co-located)
|
|
97
|
+
└── __tests__/
|
|
98
|
+
└── service.int.ts # Integration tests (separate)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Naming Convention
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Pattern: should_expectedBehavior_when_condition
|
|
105
|
+
it('should return user profile when authenticated', async () => {});
|
|
106
|
+
it('should throw UnauthorizedError when token expired', async () => {});
|
|
107
|
+
it('should reject when wallet limit exceeded', async () => {});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### AAA Pattern
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
it('should calculate balance from transactions', async () => {
|
|
114
|
+
// Arrange
|
|
115
|
+
const wallet = createTestWallet({ id: 'wallet-1' });
|
|
116
|
+
const transactions = [
|
|
117
|
+
createTestTransaction({ amount: -100 }),
|
|
118
|
+
createTestTransaction({ amount: -50 }),
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
// Act
|
|
122
|
+
const balance = calculateBalance(wallet, transactions);
|
|
123
|
+
|
|
124
|
+
// Assert
|
|
125
|
+
expect(balance).toBe(-150);
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Test Data
|
|
130
|
+
|
|
131
|
+
### Factories (Recommended)
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// packages/testing/src/factories/user.ts
|
|
135
|
+
export function createTestUser(overrides: Partial<User> = {}): User {
|
|
136
|
+
return {
|
|
137
|
+
id: `user_${cuid()}`,
|
|
138
|
+
email: `test-${Date.now()}@example.com`,
|
|
139
|
+
firstName: 'Test',
|
|
140
|
+
lastName: 'User',
|
|
141
|
+
createdAt: new Date(),
|
|
142
|
+
...overrides,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Rules
|
|
148
|
+
- Never use production data
|
|
149
|
+
- Generate unique IDs per test
|
|
150
|
+
- Clean up in `afterEach`
|
|
151
|
+
- Use realistic but fake data
|
|
152
|
+
|
|
153
|
+
## Integration Tests
|
|
154
|
+
|
|
155
|
+
### Skip When Services Unavailable
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
describe.skipIf(!process.env.DATABASE_URL)('Database Integration', () => {
|
|
159
|
+
// Tests requiring real database
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Clean Up Test Data
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
afterEach(async () => {
|
|
167
|
+
await db.delete(users).where(like(users.email, 'test-%'));
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## E2E Tests
|
|
172
|
+
|
|
173
|
+
### Critical Paths Only
|
|
174
|
+
|
|
175
|
+
Test these flows E2E:
|
|
176
|
+
- Authentication (login, logout, refresh)
|
|
177
|
+
- Money movement (create transaction, transfer)
|
|
178
|
+
- Data integrity (export, deletion)
|
|
179
|
+
|
|
180
|
+
### Page Object Pattern
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// e2e/pages/login.page.ts
|
|
184
|
+
export class LoginPage {
|
|
185
|
+
constructor(private page: Page) {}
|
|
186
|
+
|
|
187
|
+
async login(email: string, password: string) {
|
|
188
|
+
await this.page.fill('[data-testid="email"]', email);
|
|
189
|
+
await this.page.fill('[data-testid="password"]', password);
|
|
190
|
+
await this.page.click('[data-testid="submit"]');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Timeouts
|
|
196
|
+
|
|
197
|
+
- Max 30s per test
|
|
198
|
+
- Record video on failure
|
|
199
|
+
- Screenshot on assertion failure
|
|
200
|
+
|
|
201
|
+
## Security Testing
|
|
202
|
+
|
|
203
|
+
### SAST (Every PR)
|
|
204
|
+
- CodeQL for JavaScript/TypeScript
|
|
205
|
+
- npm audit for dependencies
|
|
206
|
+
- Block merge on critical findings
|
|
207
|
+
|
|
208
|
+
### DAST (Weekly)
|
|
209
|
+
- OWASP ZAP scan
|
|
210
|
+
- API fuzzing
|
|
211
|
+
- Authentication bypass checks
|
|
212
|
+
|
|
213
|
+
## Performance Testing
|
|
214
|
+
|
|
215
|
+
### Baselines
|
|
216
|
+
|
|
217
|
+
| Metric | Target | Maximum |
|
|
218
|
+
|--------|--------|---------|
|
|
219
|
+
| API p95 latency | < 200ms | < 500ms |
|
|
220
|
+
| API p99 latency | < 500ms | < 2s |
|
|
221
|
+
| DB query p95 | < 50ms | < 200ms |
|
|
222
|
+
|
|
223
|
+
### Regression Detection
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Compare current run to baseline
|
|
227
|
+
k6 run --out json=results.json tests/load/api.js
|
|
228
|
+
# Fail if p95 > baseline + 20%
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Flaky Tests
|
|
232
|
+
|
|
233
|
+
### Prevention
|
|
234
|
+
- Use `vi.useFakeTimers()` for time-dependent tests
|
|
235
|
+
- Avoid `sleep()` - use `waitFor()` or polling
|
|
236
|
+
- Isolate test state (no shared mutable state)
|
|
237
|
+
|
|
238
|
+
### Handling
|
|
239
|
+
- Max 3 retries in CI
|
|
240
|
+
- Quarantine after 3 consecutive failures
|
|
241
|
+
- Fix within 48 hours or delete
|
|
242
|
+
- Track flaky test metrics
|
|
243
|
+
|
|
244
|
+
## CI Integration
|
|
245
|
+
|
|
246
|
+
### Quality Gate
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
pnpm quality # typecheck → lint → test
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Must pass before:
|
|
253
|
+
- Merge to main
|
|
254
|
+
- Release tags
|
|
255
|
+
- Deployment
|
|
256
|
+
|
|
257
|
+
### Coverage Gate
|
|
258
|
+
|
|
259
|
+
```yaml
|
|
260
|
+
# .github/workflows/ci.yml
|
|
261
|
+
- name: Check coverage
|
|
262
|
+
run: |
|
|
263
|
+
pnpm test:coverage
|
|
264
|
+
# Fail if below threshold
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Checklist Before PR
|
|
268
|
+
|
|
269
|
+
- [ ] Unit tests for new business logic
|
|
270
|
+
- [ ] Error cases tested
|
|
271
|
+
- [ ] Edge cases tested
|
|
272
|
+
- [ ] Integration test if touching DB/Redis
|
|
273
|
+
- [ ] E2E test if critical user flow
|
|
274
|
+
- [ ] No `console.log` in test files
|
|
275
|
+
- [ ] No `.only` or `.skip` committed
|
|
276
|
+
- [ ] Coverage not decreased
|