@raftlabs/raftstack 1.9.3 → 1.10.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/.claude/commands/raftstack/inject.md +1 -0
- package/.claude/commands/raftstack/shape.md +10 -6
- package/.claude/skills/tdd/SKILL.md +586 -0
- package/dist/cli.js +505 -44
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
|
@@ -80,6 +80,7 @@ Based on the detected domain, identify relevant RaftStack skills:
|
|
|
80
80
|
|
|
81
81
|
| Domain | Skill | Path |
|
|
82
82
|
|--------|-------|------|
|
|
83
|
+
| Testing/TDD | Test-Driven Development | `.claude/skills/tdd/SKILL.md` |
|
|
83
84
|
| React/Frontend | React Development | `.claude/skills/react/SKILL.md` |
|
|
84
85
|
| API/Backend | Backend Development | `.claude/skills/backend/SKILL.md` |
|
|
85
86
|
| Database | Database Design | `.claude/skills/database/SKILL.md` |
|
|
@@ -92,9 +92,11 @@ Output format:
|
|
|
92
92
|
**Scope:** [What needs to change]
|
|
93
93
|
**Files:** [1-2 files]
|
|
94
94
|
**Approach:** [Brief description]
|
|
95
|
+
**Tests First:** [What test to write before implementation]
|
|
95
96
|
|
|
96
97
|
### 🔌 Use These Plugins
|
|
97
98
|
- [Plugin based on domain detected]
|
|
99
|
+
- `tdd` (mandatory for all implementation work)
|
|
98
100
|
```
|
|
99
101
|
|
|
100
102
|
#### ⚠️ PLANNING GATE (Quick Flow)
|
|
@@ -141,13 +143,15 @@ Output format:
|
|
|
141
143
|
[Reference similar code in the codebase to follow]
|
|
142
144
|
|
|
143
145
|
### Implementation Plan
|
|
144
|
-
1.
|
|
145
|
-
2. [
|
|
146
|
-
3.
|
|
146
|
+
1. Write failing test for [core behavior]
|
|
147
|
+
2. Implement [step with file path]
|
|
148
|
+
3. Verify test passes
|
|
149
|
+
4. Add edge case tests
|
|
147
150
|
|
|
148
151
|
### 🔌 Plugins to Use
|
|
149
152
|
| Plugin | Purpose | When |
|
|
150
153
|
|--------|---------|------|
|
|
154
|
+
| `tdd` | Test-first development | All implementation work (mandatory) |
|
|
151
155
|
| [Plugin] | [Why needed] | [Trigger condition] |
|
|
152
156
|
|
|
153
157
|
**Important:** Always use `context7` when researching libraries or getting documentation.
|
|
@@ -200,9 +204,9 @@ Before implementing:
|
|
|
200
204
|
- Error handling strategy
|
|
201
205
|
|
|
202
206
|
4. **Break into phases:**
|
|
203
|
-
- Phase 1: Core
|
|
204
|
-
- Phase 2:
|
|
205
|
-
- Phase 3:
|
|
207
|
+
- Phase 1: Core tests (TDD) - [N test files]
|
|
208
|
+
- Phase 2: Core functionality - [N files]
|
|
209
|
+
- Phase 3: Edge cases/polish - [N files]
|
|
206
210
|
|
|
207
211
|
5. **Spec folder location:**
|
|
208
212
|
|
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tdd
|
|
3
|
+
description: Use when implementing any feature or bugfix, before writing implementation code. Enforces test-first development methodology.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Test-Driven Development (TDD)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
TDD is non-negotiable: **tests come before implementation**. This skill enforces the Red-Green-Refactor cycle and ensures 80% minimum coverage.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- **Before** writing any new feature
|
|
15
|
+
- **Before** fixing any bug
|
|
16
|
+
- When adding new functions, classes, or modules
|
|
17
|
+
- When modifying existing logic
|
|
18
|
+
|
|
19
|
+
**Always** - unless you're just reading/exploring code.
|
|
20
|
+
|
|
21
|
+
## The TDD Cycle
|
|
22
|
+
|
|
23
|
+
### 1. Red: Write a Failing Test
|
|
24
|
+
|
|
25
|
+
Before writing any production code, write a test that fails.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// ❌ First, write the test that fails
|
|
29
|
+
describe('UserService', () => {
|
|
30
|
+
it('should create a new user with hashed password', async () => {
|
|
31
|
+
const service = new UserService();
|
|
32
|
+
const user = await service.createUser({
|
|
33
|
+
email: 'test@example.com',
|
|
34
|
+
password: 'SecureP@ss123'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(user.email).toBe('test@example.com');
|
|
38
|
+
expect(user.password).not.toBe('SecureP@ss123'); // Should be hashed
|
|
39
|
+
expect(user.password).toMatch(/^\$2[aby]\$/); // bcrypt format
|
|
40
|
+
expect(user.id).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Run the test**: It should fail because `createUser` doesn't exist yet.
|
|
46
|
+
|
|
47
|
+
### 2. Green: Write Minimal Code to Pass
|
|
48
|
+
|
|
49
|
+
Write **only enough code** to make the test pass. Don't over-engineer.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// ✅ Minimal implementation
|
|
53
|
+
class UserService {
|
|
54
|
+
async createUser(data: { email: string; password: string }) {
|
|
55
|
+
const hashedPassword = await bcrypt.hash(data.password, 10);
|
|
56
|
+
return {
|
|
57
|
+
id: crypto.randomUUID(),
|
|
58
|
+
email: data.email,
|
|
59
|
+
password: hashedPassword,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Run the test**: It should now pass.
|
|
66
|
+
|
|
67
|
+
### 3. Refactor: Improve While Tests Stay Green
|
|
68
|
+
|
|
69
|
+
Now improve the code without changing behavior. Tests protect you.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// ✅ Refactored with better structure
|
|
73
|
+
class UserService {
|
|
74
|
+
async createUser(data: CreateUserDTO): Promise<User> {
|
|
75
|
+
const hashedPassword = await this.hashPassword(data.password);
|
|
76
|
+
return this.buildUser(data.email, hashedPassword);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async hashPassword(password: string): Promise<string> {
|
|
80
|
+
const SALT_ROUNDS = 10;
|
|
81
|
+
return bcrypt.hash(password, SALT_ROUNDS);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private buildUser(email: string, hashedPassword: string): User {
|
|
85
|
+
return {
|
|
86
|
+
id: crypto.randomUUID(),
|
|
87
|
+
email,
|
|
88
|
+
password: hashedPassword,
|
|
89
|
+
createdAt: new Date(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Run tests**: They should still pass after refactoring.
|
|
96
|
+
|
|
97
|
+
## Test File Organization
|
|
98
|
+
|
|
99
|
+
### Co-located Tests (Recommended for Single Packages)
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
src/
|
|
103
|
+
services/
|
|
104
|
+
user.service.ts
|
|
105
|
+
user.service.test.ts ← Test next to source
|
|
106
|
+
utils/
|
|
107
|
+
crypto.ts
|
|
108
|
+
crypto.test.ts
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `__tests__` Directory (Recommended for Complex Modules)
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
src/
|
|
115
|
+
auth/
|
|
116
|
+
__tests__/
|
|
117
|
+
login.test.ts
|
|
118
|
+
register.test.ts
|
|
119
|
+
oauth.test.ts
|
|
120
|
+
login.ts
|
|
121
|
+
register.ts
|
|
122
|
+
oauth.ts
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Test Categories by Layer
|
|
126
|
+
|
|
127
|
+
### Unit Tests: Functions, Hooks, Utilities
|
|
128
|
+
|
|
129
|
+
Test individual functions in isolation. Mock dependencies.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// ✅ Unit test: Pure function
|
|
133
|
+
describe('calculateDiscount', () => {
|
|
134
|
+
it('should apply 10% discount for orders over $100', () => {
|
|
135
|
+
expect(calculateDiscount(150)).toBe(15);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return 0 for orders under threshold', () => {
|
|
139
|
+
expect(calculateDiscount(50)).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ✅ Unit test: React hook (mocked dependencies)
|
|
144
|
+
describe('useAuth', () => {
|
|
145
|
+
it('should return user data when authenticated', () => {
|
|
146
|
+
vi.spyOn(authApi, 'getCurrentUser').mockResolvedValue({
|
|
147
|
+
id: '1',
|
|
148
|
+
email: 'test@example.com'
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const { result } = renderHook(() => useAuth());
|
|
152
|
+
|
|
153
|
+
await waitFor(() => {
|
|
154
|
+
expect(result.current.user).toEqual({
|
|
155
|
+
id: '1',
|
|
156
|
+
email: 'test@example.com'
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Integration Tests: API Routes, Database Queries
|
|
164
|
+
|
|
165
|
+
Test multiple components working together. Use real dependencies or test doubles.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// ✅ Integration test: API route with database
|
|
169
|
+
describe('POST /api/users', () => {
|
|
170
|
+
it('should create user and return 201', async () => {
|
|
171
|
+
const response = await request(app)
|
|
172
|
+
.post('/api/users')
|
|
173
|
+
.send({
|
|
174
|
+
email: 'new@example.com',
|
|
175
|
+
password: 'SecureP@ss123'
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(response.status).toBe(201);
|
|
179
|
+
expect(response.body).toHaveProperty('id');
|
|
180
|
+
|
|
181
|
+
// Verify database was updated
|
|
182
|
+
const user = await db.users.findByEmail('new@example.com');
|
|
183
|
+
expect(user).toBeDefined();
|
|
184
|
+
expect(user.password).not.toBe('SecureP@ss123');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### E2E Tests: User Flows (Optional)
|
|
190
|
+
|
|
191
|
+
Test complete user journeys from UI to database. Use for critical paths only.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// ✅ E2E test: User registration flow
|
|
195
|
+
test('user can sign up and access dashboard', async ({ page }) => {
|
|
196
|
+
await page.goto('/signup');
|
|
197
|
+
await page.fill('[name="email"]', 'user@example.com');
|
|
198
|
+
await page.fill('[name="password"]', 'SecureP@ss123');
|
|
199
|
+
await page.click('button[type="submit"]');
|
|
200
|
+
|
|
201
|
+
await expect(page).toHaveURL('/dashboard');
|
|
202
|
+
await expect(page.locator('h1')).toContainText('Welcome');
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Coverage Requirements
|
|
207
|
+
|
|
208
|
+
### Minimum: 80%
|
|
209
|
+
|
|
210
|
+
The pre-push hook enforces 80% coverage for:
|
|
211
|
+
- Lines
|
|
212
|
+
- Functions
|
|
213
|
+
- Branches
|
|
214
|
+
- Statements
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Check coverage
|
|
218
|
+
npm run test:coverage
|
|
219
|
+
|
|
220
|
+
# View detailed report
|
|
221
|
+
open coverage/index.html
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Critical Paths: 100%
|
|
225
|
+
|
|
226
|
+
Aim for 100% coverage on:
|
|
227
|
+
- Authentication/authorization logic
|
|
228
|
+
- Payment processing
|
|
229
|
+
- Data validation
|
|
230
|
+
- Security-sensitive code
|
|
231
|
+
|
|
232
|
+
### What NOT to Test
|
|
233
|
+
|
|
234
|
+
Don't write tests for:
|
|
235
|
+
- Third-party libraries (trust their tests)
|
|
236
|
+
- Generated code (e.g., Prisma client)
|
|
237
|
+
- Simple getters/setters with no logic
|
|
238
|
+
- Configuration files
|
|
239
|
+
|
|
240
|
+
## Testing Patterns
|
|
241
|
+
|
|
242
|
+
### Use Test Fixtures
|
|
243
|
+
|
|
244
|
+
Create reusable test data:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// fixtures/user.fixture.ts
|
|
248
|
+
export const mockUser = (overrides = {}) => ({
|
|
249
|
+
id: '1',
|
|
250
|
+
email: 'test@example.com',
|
|
251
|
+
role: 'user',
|
|
252
|
+
createdAt: new Date('2024-01-01'),
|
|
253
|
+
...overrides,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Usage
|
|
257
|
+
it('should update user profile', async () => {
|
|
258
|
+
const user = mockUser({ email: 'original@example.com' });
|
|
259
|
+
const updated = await service.updateEmail(user, 'new@example.com');
|
|
260
|
+
expect(updated.email).toBe('new@example.com');
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Use Test Helpers
|
|
265
|
+
|
|
266
|
+
Extract common setup:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// test-helpers/setup.ts
|
|
270
|
+
export function createTestContext() {
|
|
271
|
+
const db = createTestDatabase();
|
|
272
|
+
const cache = createTestCache();
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
db,
|
|
276
|
+
cache,
|
|
277
|
+
cleanup: async () => {
|
|
278
|
+
await db.cleanup();
|
|
279
|
+
await cache.cleanup();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Usage
|
|
285
|
+
describe('OrderService', () => {
|
|
286
|
+
let context: Awaited<ReturnType<typeof createTestContext>>;
|
|
287
|
+
|
|
288
|
+
beforeEach(async () => {
|
|
289
|
+
context = await createTestContext();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
afterEach(async () => {
|
|
293
|
+
await context.cleanup();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should create order', async () => {
|
|
297
|
+
const service = new OrderService(context.db);
|
|
298
|
+
// ...
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Parameterized Tests
|
|
304
|
+
|
|
305
|
+
Test multiple cases efficiently:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// ✅ Parameterized test
|
|
309
|
+
describe('validateEmail', () => {
|
|
310
|
+
it.each([
|
|
311
|
+
['valid@example.com', true],
|
|
312
|
+
['user+tag@domain.co.uk', true],
|
|
313
|
+
['invalid@', false],
|
|
314
|
+
['@domain.com', false],
|
|
315
|
+
['no-at-sign.com', false],
|
|
316
|
+
])('should validate "%s" as %s', (email, expected) => {
|
|
317
|
+
expect(validateEmail(email)).toBe(expected);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Mocking Guidelines
|
|
323
|
+
|
|
324
|
+
### When to Mock
|
|
325
|
+
|
|
326
|
+
Mock external dependencies:
|
|
327
|
+
- HTTP requests (use `msw` or `nock`)
|
|
328
|
+
- Database calls (in unit tests)
|
|
329
|
+
- File system operations
|
|
330
|
+
- Date/time (for deterministic tests)
|
|
331
|
+
- Random values
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
// ✅ Mock external API
|
|
335
|
+
import { http, HttpResponse } from 'msw';
|
|
336
|
+
import { setupServer } from 'msw/node';
|
|
337
|
+
|
|
338
|
+
const server = setupServer(
|
|
339
|
+
http.get('https://api.example.com/users/:id', ({ params }) => {
|
|
340
|
+
return HttpResponse.json({
|
|
341
|
+
id: params.id,
|
|
342
|
+
name: 'Test User'
|
|
343
|
+
});
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
beforeAll(() => server.listen());
|
|
348
|
+
afterEach(() => server.resetHandlers());
|
|
349
|
+
afterAll(() => server.close());
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### What NOT to Mock
|
|
353
|
+
|
|
354
|
+
Don't mock your own domain logic:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// ❌ BAD: Mocking the thing you're testing
|
|
358
|
+
it('should calculate total price', () => {
|
|
359
|
+
vi.spyOn(calculator, 'calculateTotal').mockReturnValue(100);
|
|
360
|
+
expect(calculator.calculateTotal(items)).toBe(100);
|
|
361
|
+
// This test is meaningless!
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ✅ GOOD: Test actual implementation
|
|
365
|
+
it('should calculate total price', () => {
|
|
366
|
+
const items = [
|
|
367
|
+
{ price: 10, quantity: 2 },
|
|
368
|
+
{ price: 15, quantity: 1 }
|
|
369
|
+
];
|
|
370
|
+
expect(calculator.calculateTotal(items)).toBe(35); // 10*2 + 15*1
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Git Hooks Integration
|
|
375
|
+
|
|
376
|
+
### Pre-commit Hook
|
|
377
|
+
|
|
378
|
+
Runs **related tests** for changed files:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
# Triggered automatically on commit
|
|
382
|
+
# Uses: vitest related --run --passWithNoTests
|
|
383
|
+
# or: jest --bail --findRelatedTests --passWithNoTests
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Fast**: Only tests affected by your changes.
|
|
387
|
+
|
|
388
|
+
### Pre-push Hook
|
|
389
|
+
|
|
390
|
+
Runs **full test suite with coverage**:
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
# Triggered automatically on push
|
|
394
|
+
# Uses: npm run test:coverage
|
|
395
|
+
# Blocks push if tests fail or coverage < 80%
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**Bypass** (not recommended):
|
|
399
|
+
```bash
|
|
400
|
+
git push --no-verify
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Test Naming Conventions
|
|
404
|
+
|
|
405
|
+
### Use "should" statements
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// ✅ GOOD: Clear intent
|
|
409
|
+
it('should return 404 when user not found', async () => { });
|
|
410
|
+
it('should hash password before saving to database', async () => { });
|
|
411
|
+
it('should throw error for invalid email format', async () => { });
|
|
412
|
+
|
|
413
|
+
// ❌ BAD: Vague
|
|
414
|
+
it('user not found', async () => { });
|
|
415
|
+
it('password', async () => { });
|
|
416
|
+
it('works', async () => { });
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Group with `describe`
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
describe('UserService', () => {
|
|
423
|
+
describe('createUser', () => {
|
|
424
|
+
it('should create user with hashed password', async () => { });
|
|
425
|
+
it('should throw error for duplicate email', async () => { });
|
|
426
|
+
it('should validate email format', async () => { });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('deleteUser', () => {
|
|
430
|
+
it('should soft delete user by default', async () => { });
|
|
431
|
+
it('should hard delete when force=true', async () => { });
|
|
432
|
+
it('should return 404 for non-existent user', async () => { });
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Framework-Specific Setup
|
|
438
|
+
|
|
439
|
+
### Vitest Configuration
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// vitest.config.ts
|
|
443
|
+
import { defineConfig } from 'vitest/config';
|
|
444
|
+
|
|
445
|
+
export default defineConfig({
|
|
446
|
+
test: {
|
|
447
|
+
globals: true,
|
|
448
|
+
environment: 'node', // or 'jsdom' for React
|
|
449
|
+
setupFiles: ['./vitest.setup.ts'],
|
|
450
|
+
coverage: {
|
|
451
|
+
provider: 'v8',
|
|
452
|
+
reporter: ['text', 'json', 'html', 'lcov'],
|
|
453
|
+
thresholds: {
|
|
454
|
+
lines: 80,
|
|
455
|
+
functions: 80,
|
|
456
|
+
branches: 80,
|
|
457
|
+
statements: 80,
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Jest Configuration
|
|
465
|
+
|
|
466
|
+
```javascript
|
|
467
|
+
// jest.config.js
|
|
468
|
+
module.exports = {
|
|
469
|
+
preset: 'ts-jest',
|
|
470
|
+
testEnvironment: 'node',
|
|
471
|
+
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
|
472
|
+
collectCoverageFrom: [
|
|
473
|
+
'src/**/*.{ts,tsx}',
|
|
474
|
+
'!src/**/*.d.ts',
|
|
475
|
+
],
|
|
476
|
+
coverageThresholds: {
|
|
477
|
+
global: {
|
|
478
|
+
lines: 80,
|
|
479
|
+
functions: 80,
|
|
480
|
+
branches: 80,
|
|
481
|
+
statements: 80,
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## Common Mistakes
|
|
488
|
+
|
|
489
|
+
### ❌ Writing Tests After Implementation
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
// ❌ BAD: Implementation-first approach
|
|
493
|
+
// 1. Write createUser function
|
|
494
|
+
// 2. Manually test in browser
|
|
495
|
+
// 3. Write tests later (or never)
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
// ✅ GOOD: Test-first approach
|
|
500
|
+
// 1. Write failing test for createUser
|
|
501
|
+
// 2. Implement createUser to make test pass
|
|
502
|
+
// 3. Refactor with confidence
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### ❌ Testing Implementation Details
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
// ❌ BAD: Testing internal state
|
|
509
|
+
it('should set loading to true', () => {
|
|
510
|
+
component.fetchData();
|
|
511
|
+
expect(component.isLoading).toBe(true);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ✅ GOOD: Testing behavior
|
|
515
|
+
it('should display loading spinner while fetching', async () => {
|
|
516
|
+
render(<UserList />);
|
|
517
|
+
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
518
|
+
await waitFor(() => {
|
|
519
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### ❌ Not Following Arrange-Act-Assert
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
// ❌ BAD: Mixed setup and assertions
|
|
528
|
+
it('should calculate discount', () => {
|
|
529
|
+
expect(calculateDiscount(100)).toBe(10);
|
|
530
|
+
const items = [{ price: 100 }];
|
|
531
|
+
expect(calculateTotal(items)).toBe(90);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// ✅ GOOD: Clear AAA pattern
|
|
535
|
+
it('should apply 10% discount to total', () => {
|
|
536
|
+
// Arrange
|
|
537
|
+
const items = [{ price: 100, quantity: 1 }];
|
|
538
|
+
const EXPECTED_DISCOUNT = 10;
|
|
539
|
+
|
|
540
|
+
// Act
|
|
541
|
+
const discount = calculateDiscount(items);
|
|
542
|
+
const total = calculateTotal(items, discount);
|
|
543
|
+
|
|
544
|
+
// Assert
|
|
545
|
+
expect(discount).toBe(EXPECTED_DISCOUNT);
|
|
546
|
+
expect(total).toBe(90);
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
## Tools & Libraries
|
|
551
|
+
|
|
552
|
+
### Testing Frameworks
|
|
553
|
+
- **Vitest** - Fast, ESM-native, Vite-compatible
|
|
554
|
+
- **Jest** - Mature ecosystem, widely adopted
|
|
555
|
+
|
|
556
|
+
### Assertion Libraries
|
|
557
|
+
- Built-in assertions (Vitest/Jest)
|
|
558
|
+
- `@testing-library/jest-dom` - DOM matchers
|
|
559
|
+
|
|
560
|
+
### Mocking
|
|
561
|
+
- `msw` - Mock HTTP requests (recommended)
|
|
562
|
+
- `vi.mock()` / `jest.mock()` - Module mocking
|
|
563
|
+
- `vi.fn()` / `jest.fn()` - Function spies
|
|
564
|
+
|
|
565
|
+
### React Testing
|
|
566
|
+
- `@testing-library/react` - Component testing
|
|
567
|
+
- `@testing-library/user-event` - User interactions
|
|
568
|
+
- `@testing-library/react-hooks` - Hook testing
|
|
569
|
+
|
|
570
|
+
## Checklist
|
|
571
|
+
|
|
572
|
+
Before committing:
|
|
573
|
+
- [ ] Tests written **before** implementation
|
|
574
|
+
- [ ] All tests pass (`npm run test`)
|
|
575
|
+
- [ ] Coverage ≥ 80% (`npm run test:coverage`)
|
|
576
|
+
- [ ] Tests follow AAA pattern
|
|
577
|
+
- [ ] Test names use "should" statements
|
|
578
|
+
- [ ] No implementation details tested
|
|
579
|
+
- [ ] Mocks used only for external dependencies
|
|
580
|
+
|
|
581
|
+
## Resources
|
|
582
|
+
|
|
583
|
+
- [Vitest Documentation](https://vitest.dev/)
|
|
584
|
+
- [Jest Documentation](https://jestjs.io/)
|
|
585
|
+
- [Testing Library](https://testing-library.com/)
|
|
586
|
+
- [MSW Documentation](https://mswjs.io/)
|