clearctx 3.0.0 → 3.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/README.md +1 -0
- package/bin/setup.js +33 -1
- package/package.json +3 -2
- package/skills/api-design/SKILL.md +796 -0
- package/skills/devops/SKILL.md +1043 -0
- package/skills/index.json +53 -0
- package/skills/nodejs-backend/SKILL.md +853 -0
- package/skills/postgresql/SKILL.md +315 -0
- package/skills/react-frontend/SKILL.md +683 -0
- package/skills/security/SKILL.md +1000 -0
- package/skills/testing-qa/SKILL.md +842 -0
- package/skills/typescript/SKILL.md +932 -0
- package/src/mcp-server.js +126 -1
- package/src/prompts.js +47 -2
- package/src/skill-registry.js +182 -0
- package/src/stream-session.js +22 -2
- package/STRATEGY.md +0 -485
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-qa
|
|
3
|
+
description: Production-grade testing strategy, unit/integration/e2e patterns, mocking, test data, and CI integration
|
|
4
|
+
domain: testing
|
|
5
|
+
keywords: [testing, unit-tests, integration-tests, e2e, mocking, fixtures, coverage, ci, jest, vitest]
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Testing & QA — Expertise Guide
|
|
10
|
+
|
|
11
|
+
## Worker Context
|
|
12
|
+
|
|
13
|
+
You are a testing specialist. Your role: design test strategies, write comprehensive test suites, and ensure code quality through automated testing. You apply the testing pyramid, write fast deterministic tests, and integrate with CI/CD pipelines.
|
|
14
|
+
|
|
15
|
+
### Testing Pyramid (Token Budget Distribution)
|
|
16
|
+
|
|
17
|
+
| Level | Coverage | Speed | Isolation | Cost | When to Use |
|
|
18
|
+
|-------|----------|-------|-----------|------|-------------|
|
|
19
|
+
| **Unit** | 70% | <10ms | Full | Low | Business logic, utilities, pure functions |
|
|
20
|
+
| **Integration** | 20% | <500ms | Partial | Medium | API endpoints, DB operations, service interactions |
|
|
21
|
+
| **E2E** | 10% | >2s | None | High | Critical user flows only (checkout, signup, payment) |
|
|
22
|
+
|
|
23
|
+
**Decision Tree:**
|
|
24
|
+
1. Testing pure function/utility? → Unit test
|
|
25
|
+
2. Testing API endpoint? → Integration test (real DB, mocked external APIs)
|
|
26
|
+
3. Testing multi-page user flow? → E2E test (only if critical business path)
|
|
27
|
+
4. Testing UI component? → Unit test (render + interactions), NOT snapshot unless stable contract
|
|
28
|
+
|
|
29
|
+
### AAA Pattern (CRITICAL: Use for ALL tests)
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
// GOOD: Clear AAA structure
|
|
33
|
+
describe('OrderService', () => {
|
|
34
|
+
describe('createOrder', () => {
|
|
35
|
+
it('should return 201 when valid order data provided', async () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const validOrder = { userId: 1, items: [{ id: 101, qty: 2 }] };
|
|
38
|
+
const mockDb = { insert: jest.fn().mockResolvedValue({ id: 5 }) };
|
|
39
|
+
const service = new OrderService(mockDb);
|
|
40
|
+
|
|
41
|
+
// Act
|
|
42
|
+
const result = await service.createOrder(validOrder);
|
|
43
|
+
|
|
44
|
+
// Assert
|
|
45
|
+
expect(result.status).toBe(201);
|
|
46
|
+
expect(result.data.id).toBe(5);
|
|
47
|
+
expect(mockDb.insert).toHaveBeenCalledWith(validOrder);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// BAD: Mixed concerns, unclear phases
|
|
53
|
+
it('order test', async () => {
|
|
54
|
+
const service = new OrderService(mockDb);
|
|
55
|
+
const result = await service.createOrder({ userId: 1, items: [] });
|
|
56
|
+
expect(mockDb.insert).toHaveBeenCalled();
|
|
57
|
+
expect(result.status).toBe(201); // Assert before full setup clear
|
|
58
|
+
const validOrder = { userId: 1, items: [{ id: 101, qty: 2 }] };
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Mocking Decision Tree (CRITICAL: Follow this exactly)
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Is this what I'm testing?
|
|
66
|
+
├─ YES → NEVER mock it
|
|
67
|
+
└─ NO → Continue...
|
|
68
|
+
│
|
|
69
|
+
Is this an external dependency?
|
|
70
|
+
├─ External API/service → ALWAYS mock (network unreliable)
|
|
71
|
+
├─ Database
|
|
72
|
+
│ ├─ Unit test → Mock (speed + isolation)
|
|
73
|
+
│ └─ Integration test → Real DB with transaction rollback
|
|
74
|
+
├─ Time/Date → ALWAYS mock (determinism)
|
|
75
|
+
├─ File system → Mock for unit, real tmpdir for integration
|
|
76
|
+
└─ Internal module
|
|
77
|
+
├─ Simple logic → Use real implementation (test integration)
|
|
78
|
+
└─ Complex/slow → Mock ONLY if needed for isolation
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Examples:**
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// GOOD: Mock external API, use real internal services
|
|
85
|
+
describe('PaymentProcessor', () => {
|
|
86
|
+
it('should charge customer via Stripe', async () => {
|
|
87
|
+
// Mock external Stripe API
|
|
88
|
+
const mockStripe = { charge: jest.fn().mockResolvedValue({ id: 'ch_123' }) };
|
|
89
|
+
|
|
90
|
+
// Use REAL internal services (no mocking)
|
|
91
|
+
const realOrderService = new OrderService(testDb);
|
|
92
|
+
const processor = new PaymentProcessor(mockStripe, realOrderService);
|
|
93
|
+
|
|
94
|
+
const result = await processor.processPayment(orderId);
|
|
95
|
+
|
|
96
|
+
expect(mockStripe.charge).toHaveBeenCalledWith(expect.objectContaining({
|
|
97
|
+
amount: 5000,
|
|
98
|
+
currency: 'usd'
|
|
99
|
+
}));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// BAD: Overmocking — test proves nothing
|
|
104
|
+
it('should process payment', async () => {
|
|
105
|
+
const mockStripe = { charge: jest.fn().mockResolvedValue({ id: 'ch_123' }) };
|
|
106
|
+
const mockOrderService = { getOrder: jest.fn().mockResolvedValue({ total: 50 }) };
|
|
107
|
+
const mockLogger = { log: jest.fn() };
|
|
108
|
+
const processor = new PaymentProcessor(mockStripe, mockOrderService, mockLogger);
|
|
109
|
+
|
|
110
|
+
// You've mocked everything — this test only verifies your mocks work
|
|
111
|
+
const result = await processor.processPayment(1);
|
|
112
|
+
expect(result.id).toBe('ch_123'); // Circular: you mocked this response
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// GOOD: Always mock time
|
|
116
|
+
describe('SubscriptionService', () => {
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
jest.useFakeTimers();
|
|
119
|
+
jest.setSystemTime(new Date('2026-02-15'));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
jest.useRealTimers();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should expire subscription after 30 days', () => {
|
|
127
|
+
const service = new SubscriptionService();
|
|
128
|
+
const sub = service.createSubscription();
|
|
129
|
+
|
|
130
|
+
jest.advanceTimersByTime(30 * 24 * 60 * 60 * 1000);
|
|
131
|
+
|
|
132
|
+
expect(service.isExpired(sub)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Test Data Factories (IMPORTANT: Use for ALL test data)
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// GOOD: Deterministic factory functions
|
|
141
|
+
function createTestUser(overrides = {}) {
|
|
142
|
+
return {
|
|
143
|
+
id: 1,
|
|
144
|
+
email: 'test@example.com',
|
|
145
|
+
name: 'Test User',
|
|
146
|
+
createdAt: new Date('2026-01-01'),
|
|
147
|
+
...overrides
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createTestOrder(overrides = {}) {
|
|
152
|
+
return {
|
|
153
|
+
id: 100,
|
|
154
|
+
userId: 1,
|
|
155
|
+
items: [{ productId: 10, quantity: 2, price: 25.00 }],
|
|
156
|
+
total: 50.00,
|
|
157
|
+
status: 'pending',
|
|
158
|
+
...overrides
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Usage
|
|
163
|
+
it('should apply discount to order', () => {
|
|
164
|
+
const order = createTestOrder({ total: 100.00 });
|
|
165
|
+
const result = applyDiscount(order, 0.1);
|
|
166
|
+
expect(result.total).toBe(90.00);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// BAD: Inline data, hard to maintain, not reusable
|
|
170
|
+
it('test discount', () => {
|
|
171
|
+
const order = {
|
|
172
|
+
id: 999,
|
|
173
|
+
userId: 42,
|
|
174
|
+
items: [{ productId: 88, quantity: 1, price: 100.00 }],
|
|
175
|
+
total: 100.00,
|
|
176
|
+
status: 'pending'
|
|
177
|
+
};
|
|
178
|
+
// ... test continues
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// BAD: Non-deterministic (breaks randomly)
|
|
182
|
+
function createTestUser() {
|
|
183
|
+
return {
|
|
184
|
+
id: Math.floor(Math.random() * 10000), // NEVER use random in tests
|
|
185
|
+
email: `user${Date.now()}@example.com`, // NEVER use current time
|
|
186
|
+
name: 'Test User'
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Assertion Patterns (Prefer specific over generic)
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
// GOOD: Specific assertions
|
|
195
|
+
expect(response.status).toBe(201);
|
|
196
|
+
expect(response.body).toEqual({ id: 5, name: 'Widget' });
|
|
197
|
+
expect(user.roles).toContain('admin');
|
|
198
|
+
expect(result.errors).toHaveLength(2);
|
|
199
|
+
|
|
200
|
+
// BAD: Generic assertions (low signal)
|
|
201
|
+
expect(response).toBeTruthy(); // What are you actually testing?
|
|
202
|
+
expect(response.status).toBeGreaterThan(0); // Too broad
|
|
203
|
+
expect(result).toBeDefined(); // Useless — undefined would throw earlier
|
|
204
|
+
|
|
205
|
+
// GOOD: Custom matchers for domain concepts
|
|
206
|
+
expect.extend({
|
|
207
|
+
toBeValidOrder(received) {
|
|
208
|
+
const pass = received.id > 0 &&
|
|
209
|
+
Array.isArray(received.items) &&
|
|
210
|
+
received.total >= 0;
|
|
211
|
+
return {
|
|
212
|
+
pass,
|
|
213
|
+
message: () => `Expected ${received} to be a valid order`
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(order).toBeValidOrder();
|
|
219
|
+
|
|
220
|
+
// GOOD: Snapshot testing (use sparingly)
|
|
221
|
+
// ONLY for: API response schemas, component trees, serialized config
|
|
222
|
+
// NEVER for: dates, IDs, timestamps, random data
|
|
223
|
+
it('should return correct API response shape', () => {
|
|
224
|
+
const response = api.getUserProfile(1);
|
|
225
|
+
expect(response).toMatchSnapshot({
|
|
226
|
+
id: expect.any(Number),
|
|
227
|
+
createdAt: expect.any(String), // Ignore dynamic values
|
|
228
|
+
email: expect.any(String)
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// BAD: Snapshot with dynamic data (flaky)
|
|
233
|
+
it('snapshot test', () => {
|
|
234
|
+
const response = { id: Date.now(), user: 'test' };
|
|
235
|
+
expect(response).toMatchSnapshot(); // Fails every run
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Integration Testing Pattern
|
|
240
|
+
|
|
241
|
+
```javascript
|
|
242
|
+
// GOOD: Integration test with real DB, transaction rollback
|
|
243
|
+
describe('POST /api/orders', () => {
|
|
244
|
+
let app, db;
|
|
245
|
+
|
|
246
|
+
beforeAll(async () => {
|
|
247
|
+
db = await createTestDatabase(); // Real test DB
|
|
248
|
+
app = createApp(db);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
beforeEach(async () => {
|
|
252
|
+
await db.raw('BEGIN'); // Start transaction
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
afterEach(async () => {
|
|
256
|
+
await db.raw('ROLLBACK'); // Rollback after each test (clean slate)
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
afterAll(async () => {
|
|
260
|
+
await db.destroy();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should create order and return 201', async () => {
|
|
264
|
+
// Arrange: Seed test data
|
|
265
|
+
await db('users').insert({ id: 1, email: 'test@example.com' });
|
|
266
|
+
const orderData = { userId: 1, items: [{ id: 10, qty: 2 }] };
|
|
267
|
+
|
|
268
|
+
// Act: Make real HTTP request
|
|
269
|
+
const response = await request(app)
|
|
270
|
+
.post('/api/orders')
|
|
271
|
+
.send(orderData);
|
|
272
|
+
|
|
273
|
+
// Assert: Check response + DB state
|
|
274
|
+
expect(response.status).toBe(201);
|
|
275
|
+
expect(response.body.id).toBeGreaterThan(0);
|
|
276
|
+
|
|
277
|
+
const dbOrder = await db('orders').where({ id: response.body.id }).first();
|
|
278
|
+
expect(dbOrder.user_id).toBe(1);
|
|
279
|
+
expect(dbOrder.status).toBe('pending');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should return 400 when userId missing', async () => {
|
|
283
|
+
const response = await request(app)
|
|
284
|
+
.post('/api/orders')
|
|
285
|
+
.send({ items: [] });
|
|
286
|
+
|
|
287
|
+
expect(response.status).toBe(400);
|
|
288
|
+
expect(response.body.error).toContain('userId');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// BAD: Mocking DB in "integration" test (not actually integrated)
|
|
293
|
+
it('should create order', async () => {
|
|
294
|
+
const mockDb = { insert: jest.fn().mockResolvedValue({ id: 5 }) };
|
|
295
|
+
// This is a UNIT test pretending to be integration
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Coverage Strategy (IMPORTANT: Branch coverage > line coverage)
|
|
300
|
+
|
|
301
|
+
| Code Type | Target | Priority | Why |
|
|
302
|
+
|-----------|--------|----------|-----|
|
|
303
|
+
| Business logic | 90%+ | CRITICAL | Core value, high bug impact |
|
|
304
|
+
| API routes | 80%+ | High | User-facing contracts |
|
|
305
|
+
| Utils/helpers | 85%+ | High | Reused everywhere |
|
|
306
|
+
| Config files | 0% | Ignore | Static data |
|
|
307
|
+
| Generated code | 0% | Ignore | Not hand-written |
|
|
308
|
+
| UI components | 70%+ | Medium | Snapshot + interaction tests |
|
|
309
|
+
|
|
310
|
+
**Branch coverage example:**
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
// This function has 4 branches
|
|
314
|
+
function calculateDiscount(user, orderTotal) {
|
|
315
|
+
if (!user) return 0; // Branch 1
|
|
316
|
+
if (user.isPremium) return 0.2; // Branch 2
|
|
317
|
+
if (orderTotal > 100) return 0.1; // Branch 3
|
|
318
|
+
return 0; // Branch 4
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// GOOD: Test ALL branches
|
|
322
|
+
describe('calculateDiscount', () => {
|
|
323
|
+
it('should return 0 when user is null', () => {
|
|
324
|
+
expect(calculateDiscount(null, 50)).toBe(0);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should return 0.2 when user is premium', () => {
|
|
328
|
+
expect(calculateDiscount({ isPremium: true }, 50)).toBe(0.2);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should return 0.1 when order total > 100', () => {
|
|
332
|
+
expect(calculateDiscount({ isPremium: false }, 150)).toBe(0.1);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should return 0 when order total <= 100 and not premium', () => {
|
|
336
|
+
expect(calculateDiscount({ isPremium: false }, 50)).toBe(0);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// BAD: Only 50% branch coverage
|
|
341
|
+
describe('calculateDiscount', () => {
|
|
342
|
+
it('should calculate discount', () => {
|
|
343
|
+
expect(calculateDiscount({ isPremium: true }, 50)).toBe(0.2);
|
|
344
|
+
expect(calculateDiscount({ isPremium: false }, 50)).toBe(0);
|
|
345
|
+
// Missing: null user, orderTotal > 100 cases
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**NEVER chase 100% coverage:**
|
|
351
|
+
- 80-90% is optimal (diminishing returns after)
|
|
352
|
+
- Focus on business-critical paths first
|
|
353
|
+
- Ignore defensive error handling you can't trigger
|
|
354
|
+
- Coverage is a guide, not a goal
|
|
355
|
+
|
|
356
|
+
### CI Integration Pattern
|
|
357
|
+
|
|
358
|
+
```yaml
|
|
359
|
+
# GOOD: Optimized test pipeline
|
|
360
|
+
name: Test
|
|
361
|
+
on: [push, pull_request]
|
|
362
|
+
|
|
363
|
+
jobs:
|
|
364
|
+
test:
|
|
365
|
+
runs-on: ubuntu-latest
|
|
366
|
+
steps:
|
|
367
|
+
- uses: actions/checkout@v3
|
|
368
|
+
- uses: actions/setup-node@v3
|
|
369
|
+
|
|
370
|
+
# Step 1: Unit tests (fail fast)
|
|
371
|
+
- name: Unit tests
|
|
372
|
+
run: npm run test:unit
|
|
373
|
+
timeout-minutes: 2
|
|
374
|
+
|
|
375
|
+
# Step 2: Integration tests (parallel)
|
|
376
|
+
- name: Integration tests
|
|
377
|
+
run: npm run test:integration -- --maxWorkers=4
|
|
378
|
+
timeout-minutes: 5
|
|
379
|
+
|
|
380
|
+
# Step 3: E2E tests (only if unit + integration pass)
|
|
381
|
+
- name: E2E tests
|
|
382
|
+
run: npm run test:e2e
|
|
383
|
+
timeout-minutes: 10
|
|
384
|
+
|
|
385
|
+
# Step 4: Coverage check
|
|
386
|
+
- name: Coverage check
|
|
387
|
+
run: |
|
|
388
|
+
npm run test:coverage
|
|
389
|
+
if [ $(cat coverage/coverage-summary.json | jq '.total.branches.pct') -lt 80 ]; then
|
|
390
|
+
echo "Branch coverage below 80%"
|
|
391
|
+
exit 1
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
# Step 5: Flaky test detection
|
|
395
|
+
- name: Re-run failures once
|
|
396
|
+
if: failure()
|
|
397
|
+
run: npm test -- --onlyFailures
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**CI Rules:**
|
|
401
|
+
- Run unit tests FIRST (fastest feedback)
|
|
402
|
+
- Parallelize test suites (--maxWorkers)
|
|
403
|
+
- Fail build on coverage regression (compare against main branch)
|
|
404
|
+
- Re-run failures ONCE (detect flaky tests, don't mask real failures)
|
|
405
|
+
- Separate unit/integration/e2e jobs (fail fast on unit tests)
|
|
406
|
+
|
|
407
|
+
## Conventions
|
|
408
|
+
|
|
409
|
+
### Test File Naming
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
src/
|
|
413
|
+
services/
|
|
414
|
+
OrderService.js
|
|
415
|
+
OrderService.test.js ← Unit tests next to source
|
|
416
|
+
api/
|
|
417
|
+
routes/
|
|
418
|
+
orders.js
|
|
419
|
+
orders.test.js ← Integration tests next to routes
|
|
420
|
+
__tests__/
|
|
421
|
+
e2e/
|
|
422
|
+
checkout-flow.e2e.test.js ← E2E tests in separate directory
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Rules:**
|
|
426
|
+
- Unit tests: `{module}.test.js` next to source file
|
|
427
|
+
- Integration tests: `{module}.test.js` or `{module}.integration.test.js`
|
|
428
|
+
- E2E tests: `{flow}.e2e.test.js` in `__tests__/e2e/`
|
|
429
|
+
- Factories: `__tests__/factories/{entity}.js`
|
|
430
|
+
- Fixtures: `__tests__/fixtures/{data}.json`
|
|
431
|
+
|
|
432
|
+
### Test Naming Pattern (CRITICAL: Describe behavior, not implementation)
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
// GOOD: Behavior-focused names
|
|
436
|
+
describe('OrderService', () => {
|
|
437
|
+
describe('createOrder', () => {
|
|
438
|
+
it('should return 201 when valid order data provided', () => {});
|
|
439
|
+
it('should return 400 when userId is missing', () => {});
|
|
440
|
+
it('should return 400 when items array is empty', () => {});
|
|
441
|
+
it('should calculate total by summing item prices', () => {});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
describe('cancelOrder', () => {
|
|
445
|
+
it('should set status to cancelled when order is pending', () => {});
|
|
446
|
+
it('should return 400 when order is already shipped', () => {});
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// BAD: Implementation-focused names (brittle)
|
|
451
|
+
describe('OrderService', () => {
|
|
452
|
+
it('test createOrder method', () => {});
|
|
453
|
+
it('should call db.insert', () => {}); // Testing implementation detail
|
|
454
|
+
it('works correctly', () => {}); // Vague
|
|
455
|
+
it('order test 1', () => {}); // Meaningless
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
**Template:** `should [expected behavior] when [condition]`
|
|
460
|
+
|
|
461
|
+
### Assertion Conventions
|
|
462
|
+
|
|
463
|
+
```javascript
|
|
464
|
+
// Prefer toBe for primitives
|
|
465
|
+
expect(result.status).toBe(201);
|
|
466
|
+
expect(result.isValid).toBe(true);
|
|
467
|
+
|
|
468
|
+
// Prefer toEqual for objects/arrays (deep equality)
|
|
469
|
+
expect(result.data).toEqual({ id: 5, name: 'Widget' });
|
|
470
|
+
|
|
471
|
+
// Prefer toContain for arrays
|
|
472
|
+
expect(user.roles).toContain('admin');
|
|
473
|
+
|
|
474
|
+
// Prefer toHaveLength for length checks
|
|
475
|
+
expect(errors).toHaveLength(2);
|
|
476
|
+
|
|
477
|
+
// Prefer toHaveBeenCalledWith for mock verification
|
|
478
|
+
expect(mockDb.insert).toHaveBeenCalledWith(expectedData);
|
|
479
|
+
expect(mockDb.insert).toHaveBeenCalledTimes(1);
|
|
480
|
+
|
|
481
|
+
// NEVER use toBeTruthy/toBeFalsy for specific values
|
|
482
|
+
expect(result.status).toBe(201); // NOT .toBeTruthy()
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Common Patterns
|
|
486
|
+
|
|
487
|
+
### 1. Test Isolation (beforeEach/afterEach)
|
|
488
|
+
|
|
489
|
+
```javascript
|
|
490
|
+
describe('UserService', () => {
|
|
491
|
+
let service, mockDb;
|
|
492
|
+
|
|
493
|
+
beforeEach(() => {
|
|
494
|
+
mockDb = createMockDb();
|
|
495
|
+
service = new UserService(mockDb);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
afterEach(() => {
|
|
499
|
+
jest.clearAllMocks(); // Clear call history
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('test 1', () => {
|
|
503
|
+
// service and mockDb are fresh for EACH test
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('test 2', () => {
|
|
507
|
+
// No shared state from test 1
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### 2. Async Testing (async/await)
|
|
513
|
+
|
|
514
|
+
```javascript
|
|
515
|
+
// GOOD: async/await (modern, readable)
|
|
516
|
+
it('should fetch user', async () => {
|
|
517
|
+
const user = await service.getUser(1);
|
|
518
|
+
expect(user.id).toBe(1);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// BAD: Nested promises (callback hell)
|
|
522
|
+
it('should fetch user', (done) => {
|
|
523
|
+
service.getUser(1).then((user) => {
|
|
524
|
+
expect(user.id).toBe(1);
|
|
525
|
+
done();
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### 3. Error Testing
|
|
531
|
+
|
|
532
|
+
```javascript
|
|
533
|
+
// GOOD: Test error cases explicitly
|
|
534
|
+
it('should throw when user not found', async () => {
|
|
535
|
+
mockDb.findById.mockResolvedValue(null);
|
|
536
|
+
|
|
537
|
+
await expect(service.getUser(999)).rejects.toThrow('User not found');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('should return 404 when resource missing', async () => {
|
|
541
|
+
const response = await request(app).get('/api/users/999');
|
|
542
|
+
|
|
543
|
+
expect(response.status).toBe(404);
|
|
544
|
+
expect(response.body.error).toBe('User not found');
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// GOOD: Test error validation
|
|
548
|
+
it('should validate email format', async () => {
|
|
549
|
+
const response = await request(app)
|
|
550
|
+
.post('/api/users')
|
|
551
|
+
.send({ email: 'invalid-email' });
|
|
552
|
+
|
|
553
|
+
expect(response.status).toBe(400);
|
|
554
|
+
expect(response.body.errors).toContainEqual(
|
|
555
|
+
expect.objectContaining({ field: 'email', message: 'Invalid email format' })
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### 4. Parameterized Tests (test.each)
|
|
561
|
+
|
|
562
|
+
```javascript
|
|
563
|
+
// GOOD: Reduce duplication with test.each
|
|
564
|
+
describe('calculateDiscount', () => {
|
|
565
|
+
test.each([
|
|
566
|
+
[{ isPremium: true }, 100, 0.2],
|
|
567
|
+
[{ isPremium: false }, 150, 0.1],
|
|
568
|
+
[{ isPremium: false }, 50, 0],
|
|
569
|
+
[null, 100, 0]
|
|
570
|
+
])('should return %p for user %p with total %p', (user, total, expected) => {
|
|
571
|
+
expect(calculateDiscount(user, total)).toBe(expected);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// BAD: Repetitive tests
|
|
576
|
+
it('test case 1', () => {
|
|
577
|
+
expect(calculateDiscount({ isPremium: true }, 100)).toBe(0.2);
|
|
578
|
+
});
|
|
579
|
+
it('test case 2', () => {
|
|
580
|
+
expect(calculateDiscount({ isPremium: false }, 150)).toBe(0.1);
|
|
581
|
+
});
|
|
582
|
+
// ... repeated pattern
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### 5. Spy Pattern (Verify behavior without mocking)
|
|
586
|
+
|
|
587
|
+
```javascript
|
|
588
|
+
// GOOD: Spy on real implementation
|
|
589
|
+
it('should call logger when error occurs', async () => {
|
|
590
|
+
const logger = { error: jest.fn() };
|
|
591
|
+
const service = new PaymentService(realStripe, logger);
|
|
592
|
+
|
|
593
|
+
realStripe.charge = jest.fn().mockRejectedValue(new Error('Payment failed'));
|
|
594
|
+
|
|
595
|
+
await expect(service.processPayment(1)).rejects.toThrow();
|
|
596
|
+
|
|
597
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
598
|
+
expect.stringContaining('Payment failed')
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## Anti-Patterns
|
|
604
|
+
|
|
605
|
+
### 1. Testing Implementation Details (NEVER do this)
|
|
606
|
+
|
|
607
|
+
```javascript
|
|
608
|
+
// BAD: Test breaks on refactor even if behavior unchanged
|
|
609
|
+
it('should call db.insert with correct params', () => {
|
|
610
|
+
service.createOrder(orderData);
|
|
611
|
+
expect(mockDb.insert).toHaveBeenCalled(); // Implementation detail
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// GOOD: Test observable behavior
|
|
615
|
+
it('should create order and return ID', async () => {
|
|
616
|
+
const result = await service.createOrder(orderData);
|
|
617
|
+
expect(result.id).toBeGreaterThan(0);
|
|
618
|
+
|
|
619
|
+
// Verify side effect (DB state)
|
|
620
|
+
const order = await db('orders').where({ id: result.id }).first();
|
|
621
|
+
expect(order.user_id).toBe(orderData.userId);
|
|
622
|
+
});
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Why:** Implementation can change (switch from INSERT to UPSERT, change ORM) but behavior stays same. Test behavior, not implementation.
|
|
626
|
+
|
|
627
|
+
### 2. Shared Mutable State (NEVER share state between tests)
|
|
628
|
+
|
|
629
|
+
```javascript
|
|
630
|
+
// BAD: Shared state causes order-dependent failures
|
|
631
|
+
describe('OrderService', () => {
|
|
632
|
+
let orders = []; // SHARED across tests
|
|
633
|
+
|
|
634
|
+
it('test 1', () => {
|
|
635
|
+
orders.push({ id: 1 });
|
|
636
|
+
expect(orders).toHaveLength(1);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('test 2', () => {
|
|
640
|
+
expect(orders).toHaveLength(0); // FAILS if test 1 runs first
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// GOOD: Fresh state for each test
|
|
645
|
+
describe('OrderService', () => {
|
|
646
|
+
let orders;
|
|
647
|
+
|
|
648
|
+
beforeEach(() => {
|
|
649
|
+
orders = []; // Fresh for EACH test
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('test 1', () => {
|
|
653
|
+
orders.push({ id: 1 });
|
|
654
|
+
expect(orders).toHaveLength(1);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('test 2', () => {
|
|
658
|
+
expect(orders).toHaveLength(0); // Always passes
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### 3. Testing Framework Code (NEVER do this)
|
|
664
|
+
|
|
665
|
+
```javascript
|
|
666
|
+
// BAD: Testing that Express works (not your code)
|
|
667
|
+
it('should have a POST /api/orders route', () => {
|
|
668
|
+
const routes = app._router.stack.filter(r => r.route);
|
|
669
|
+
expect(routes.some(r => r.route.path === '/api/orders')).toBe(true);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// GOOD: Test that YOUR route handler works correctly
|
|
673
|
+
it('should create order via POST /api/orders', async () => {
|
|
674
|
+
const response = await request(app)
|
|
675
|
+
.post('/api/orders')
|
|
676
|
+
.send({ userId: 1, items: [{ id: 10, qty: 2 }] });
|
|
677
|
+
|
|
678
|
+
expect(response.status).toBe(201);
|
|
679
|
+
expect(response.body.id).toBeGreaterThan(0);
|
|
680
|
+
});
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### 4. Overmocking (Test proves nothing)
|
|
684
|
+
|
|
685
|
+
```javascript
|
|
686
|
+
// BAD: Everything is mocked
|
|
687
|
+
it('should process payment', async () => {
|
|
688
|
+
const mockStripe = { charge: jest.fn().mockResolvedValue({ id: 'ch_123' }) };
|
|
689
|
+
const mockOrderService = { getOrder: jest.fn().mockResolvedValue({ total: 50 }) };
|
|
690
|
+
const mockUserService = { getUser: jest.fn().mockResolvedValue({ id: 1 }) };
|
|
691
|
+
|
|
692
|
+
const processor = new PaymentProcessor(mockStripe, mockOrderService, mockUserService);
|
|
693
|
+
const result = await processor.processPayment(1);
|
|
694
|
+
|
|
695
|
+
expect(result.id).toBe('ch_123'); // Circular: you defined this response
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// GOOD: Mock only external dependencies
|
|
699
|
+
it('should process payment via Stripe', async () => {
|
|
700
|
+
const mockStripe = { charge: jest.fn().mockResolvedValue({ id: 'ch_123' }) };
|
|
701
|
+
|
|
702
|
+
// Use REAL internal services
|
|
703
|
+
const realOrderService = new OrderService(testDb);
|
|
704
|
+
const realUserService = new UserService(testDb);
|
|
705
|
+
|
|
706
|
+
// Seed test data
|
|
707
|
+
await testDb('users').insert({ id: 1, email: 'test@example.com' });
|
|
708
|
+
await testDb('orders').insert({ id: 1, user_id: 1, total: 50.00 });
|
|
709
|
+
|
|
710
|
+
const processor = new PaymentProcessor(mockStripe, realOrderService, realUserService);
|
|
711
|
+
const result = await processor.processPayment(1);
|
|
712
|
+
|
|
713
|
+
expect(mockStripe.charge).toHaveBeenCalledWith(
|
|
714
|
+
expect.objectContaining({ amount: 5000 }) // Verify integration
|
|
715
|
+
);
|
|
716
|
+
});
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### 5. Slow Tests in Unit Suite (Move to integration)
|
|
720
|
+
|
|
721
|
+
```javascript
|
|
722
|
+
// BAD: Unit test that hits real DB (slow, 500ms+)
|
|
723
|
+
describe('OrderService (unit)', () => {
|
|
724
|
+
it('should create order', async () => {
|
|
725
|
+
await db('orders').insert({ userId: 1, total: 50 }); // Real DB call
|
|
726
|
+
// This is an INTEGRATION test
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// GOOD: Fast unit test (<10ms)
|
|
731
|
+
describe('OrderService (unit)', () => {
|
|
732
|
+
it('should calculate total correctly', () => {
|
|
733
|
+
const items = [{ price: 10, qty: 2 }, { price: 15, qty: 1 }];
|
|
734
|
+
const total = OrderService.calculateTotal(items);
|
|
735
|
+
expect(total).toBe(35);
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// GOOD: Slow test in integration suite
|
|
740
|
+
describe('OrderService (integration)', () => {
|
|
741
|
+
it('should persist order to database', async () => {
|
|
742
|
+
const result = await service.createOrder(orderData);
|
|
743
|
+
const dbOrder = await db('orders').where({ id: result.id }).first();
|
|
744
|
+
expect(dbOrder.total).toBe(50.00);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
## Integration Notes
|
|
750
|
+
|
|
751
|
+
**Multi-Session Workflow:**
|
|
752
|
+
|
|
753
|
+
You are `testing-qa` worker in a clearctx orchestrated team. Follow this protocol:
|
|
754
|
+
|
|
755
|
+
1. **Check inbox FIRST** (ENFORCED):
|
|
756
|
+
```javascript
|
|
757
|
+
await mcp__multi_session__team_check_inbox({ name: "testing-qa" })
|
|
758
|
+
```
|
|
759
|
+
Required before using artifact/contract tools.
|
|
760
|
+
|
|
761
|
+
2. **Read shared artifacts**:
|
|
762
|
+
- `db-schema` from db-dev worker (for test fixtures matching real schema)
|
|
763
|
+
- `api-contracts` from backend-dev worker (for endpoint integration tests)
|
|
764
|
+
- `shared-conventions` (for response format, error format, status codes)
|
|
765
|
+
|
|
766
|
+
3. **Publish test-results artifact**:
|
|
767
|
+
```javascript
|
|
768
|
+
await mcp__multi_session__artifact_publish({
|
|
769
|
+
artifactId: "test-results",
|
|
770
|
+
type: "test-results",
|
|
771
|
+
name: "Test Suite Results",
|
|
772
|
+
publisher: "testing-qa",
|
|
773
|
+
data: {
|
|
774
|
+
summary: {
|
|
775
|
+
total: 156,
|
|
776
|
+
passed: 154,
|
|
777
|
+
failed: 2,
|
|
778
|
+
skipped: 0
|
|
779
|
+
},
|
|
780
|
+
coverage: {
|
|
781
|
+
lines: 87.5,
|
|
782
|
+
branches: 82.3,
|
|
783
|
+
functions: 91.2
|
|
784
|
+
},
|
|
785
|
+
duration: "12.4s",
|
|
786
|
+
failedTests: [
|
|
787
|
+
{ file: "services/OrderService.test.js", test: "should handle concurrent orders", error: "..." }
|
|
788
|
+
],
|
|
789
|
+
files: [
|
|
790
|
+
"services/OrderService.test.js",
|
|
791
|
+
"api/routes/orders.test.js",
|
|
792
|
+
"__tests__/e2e/checkout-flow.e2e.test.js"
|
|
793
|
+
]
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
4. **Broadcast completion**:
|
|
799
|
+
```javascript
|
|
800
|
+
await mcp__multi_session__team_broadcast({
|
|
801
|
+
from: "testing-qa",
|
|
802
|
+
content: "✅ Test suite complete: 154/156 passed (87.5% coverage). 2 failures in OrderService concurrent handling."
|
|
803
|
+
})
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
5. **File paths**: ALWAYS use relative paths
|
|
807
|
+
- ✅ `src/services/OrderService.test.js`
|
|
808
|
+
- ❌ `E:/Projects/app/src/services/OrderService.test.js`
|
|
809
|
+
|
|
810
|
+
6. **Test file creation**: Create test files NEXT to source files (unless e2e)
|
|
811
|
+
- Unit: `src/services/OrderService.test.js` (next to `OrderService.js`)
|
|
812
|
+
- Integration: `src/api/routes/orders.test.js` (next to `orders.js`)
|
|
813
|
+
- E2E: `__tests__/e2e/checkout-flow.e2e.test.js`
|
|
814
|
+
|
|
815
|
+
7. **Communicate test failures**: If tests fail due to API contract mismatch, use `team_send_message` to backend-dev:
|
|
816
|
+
```javascript
|
|
817
|
+
await mcp__multi_session__team_send_message({
|
|
818
|
+
from: "testing-qa",
|
|
819
|
+
to: "backend-dev",
|
|
820
|
+
content: "POST /api/orders returning 200 instead of 201 per shared conventions. Expected statusCodes.create=201."
|
|
821
|
+
})
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
**Dependency Chain:**
|
|
825
|
+
1. db-dev publishes `db-schema` artifact
|
|
826
|
+
2. backend-dev publishes `api-contracts` artifact
|
|
827
|
+
3. testing-qa reads both artifacts → writes tests → publishes `test-results`
|
|
828
|
+
4. Orchestrator verifies via `artifact_readers` that testing-qa consumed upstream artifacts
|
|
829
|
+
|
|
830
|
+
**Status Management:**
|
|
831
|
+
- Auto-set to "active" on spawn (no action needed)
|
|
832
|
+
- Auto-set to "idle" when session stops (no action needed)
|
|
833
|
+
- Only use `team_update_status` for custom statuses like "blocked"
|
|
834
|
+
|
|
835
|
+
**Convention Compliance:**
|
|
836
|
+
Always follow `shared-conventions` artifact for:
|
|
837
|
+
- Response format (e.g., `{ data: <result> }`)
|
|
838
|
+
- Error format (e.g., `{ error: <message> }`)
|
|
839
|
+
- Status codes (create=201, read=200, etc.)
|
|
840
|
+
- Date formats, enum values, boolean handling
|
|
841
|
+
|
|
842
|
+
If a test fails due to convention mismatch, it's likely the implementation is wrong (not your test). Report to orchestrator.
|