omgkit 2.1.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/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,50 +1,712 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: testing-anti-patterns
|
|
3
|
-
description:
|
|
3
|
+
description: Common testing anti-patterns to avoid and how to fix them for reliable, maintainable test suites
|
|
4
|
+
category: methodology
|
|
5
|
+
triggers:
|
|
6
|
+
- testing anti-patterns
|
|
7
|
+
- flaky tests
|
|
8
|
+
- test smells
|
|
9
|
+
- bad tests
|
|
10
|
+
- test maintenance
|
|
11
|
+
- unreliable tests
|
|
12
|
+
- test code quality
|
|
4
13
|
---
|
|
5
14
|
|
|
6
|
-
# Testing Anti-Patterns
|
|
15
|
+
# Testing Anti-Patterns
|
|
7
16
|
|
|
8
|
-
|
|
17
|
+
Identify and fix **common testing anti-patterns** that lead to flaky, slow, or unmaintainable test suites. This skill provides patterns to recognize problematic tests and techniques to refactor them into reliable, valuable tests.
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Build reliable, maintainable test suites:
|
|
22
|
+
|
|
23
|
+
- Recognize testing anti-patterns quickly
|
|
24
|
+
- Fix flaky tests that undermine confidence
|
|
25
|
+
- Avoid tests that test implementation details
|
|
26
|
+
- Eliminate slow tests that hurt development velocity
|
|
27
|
+
- Remove tests that are hard to maintain
|
|
28
|
+
- Build tests that provide real value
|
|
29
|
+
- Create self-documenting test suites
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
### 1. The Testing Anti-Pattern Catalog
|
|
14
34
|
|
|
15
|
-
|
|
16
|
-
|
|
35
|
+
```markdown
|
|
36
|
+
## Common Testing Anti-Patterns
|
|
37
|
+
|
|
38
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
39
|
+
│ TESTING ANTI-PATTERN SEVERITY │
|
|
40
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
41
|
+
│ │
|
|
42
|
+
│ 🔴 CRITICAL (Fix immediately) │
|
|
43
|
+
│ ───────────────────────────── │
|
|
44
|
+
│ • Flaky tests - Random failures destroy trust │
|
|
45
|
+
│ • Testing implementation - Breaks on every refactor │
|
|
46
|
+
│ • Hidden dependencies - Tests fail mysteriously │
|
|
47
|
+
│ │
|
|
48
|
+
│ 🟡 HIGH (Fix soon) │
|
|
49
|
+
│ ───────────────── │
|
|
50
|
+
│ • Slow tests - Hurt development velocity │
|
|
51
|
+
│ • Test interdependence - Can't run tests in isolation │
|
|
52
|
+
│ • Over-mocking - Tests pass but bugs slip through │
|
|
53
|
+
│ │
|
|
54
|
+
│ 🟠 MEDIUM (Plan to fix) │
|
|
55
|
+
│ ─────────────────────── │
|
|
56
|
+
│ • Poor naming - Tests don't document behavior │
|
|
57
|
+
│ • Magic values - Unclear why values are expected │
|
|
58
|
+
│ • Giant tests - Hard to understand and maintain │
|
|
59
|
+
│ │
|
|
60
|
+
│ 🔵 LOW (Fix when touching) │
|
|
61
|
+
│ ───────────────────────── │
|
|
62
|
+
│ • Commented tests - Remove or fix them │
|
|
63
|
+
│ • Duplicate tests - Consolidate coverage │
|
|
64
|
+
│ • Dead assertions - Remove or make meaningful │
|
|
65
|
+
│ │
|
|
66
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
17
67
|
```
|
|
18
68
|
|
|
19
69
|
### 2. Flaky Tests
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
/**
|
|
73
|
+
* ANTI-PATTERN: Flaky Tests
|
|
74
|
+
* Tests that sometimes pass, sometimes fail
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
// ❌ FLAKY: Timing-dependent test
|
|
78
|
+
describe('FlakY: Timing Issues', () => {
|
|
79
|
+
it('processes data after delay', async () => {
|
|
80
|
+
startAsyncProcess();
|
|
81
|
+
|
|
82
|
+
// PROBLEM: 100ms might not be enough on slow CI
|
|
83
|
+
await sleep(100);
|
|
84
|
+
|
|
85
|
+
expect(result).toBe('processed');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ✅ FIXED: Wait for condition, not time
|
|
90
|
+
describe('Fixed: Proper async handling', () => {
|
|
91
|
+
it('processes data after delay', async () => {
|
|
92
|
+
startAsyncProcess();
|
|
93
|
+
|
|
94
|
+
// Wait for actual condition
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
expect(result).toBe('processed');
|
|
97
|
+
}, { timeout: 5000 });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ❌ FLAKY: Random data without determinism
|
|
102
|
+
describe('Flaky: Random Data', () => {
|
|
103
|
+
it('handles user data', () => {
|
|
104
|
+
const user = {
|
|
105
|
+
id: Math.random().toString(),
|
|
106
|
+
name: faker.name.fullName()
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result = processUser(user);
|
|
110
|
+
|
|
111
|
+
// PROBLEM: Can't know expected result
|
|
112
|
+
expect(result).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ✅ FIXED: Deterministic test data
|
|
117
|
+
describe('Fixed: Deterministic Data', () => {
|
|
118
|
+
it('handles user data', () => {
|
|
119
|
+
const user = {
|
|
120
|
+
id: 'user-123',
|
|
121
|
+
name: 'John Doe'
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = processUser(user);
|
|
125
|
+
|
|
126
|
+
expect(result).toEqual({
|
|
127
|
+
id: 'user-123',
|
|
128
|
+
displayName: 'John Doe',
|
|
129
|
+
slug: 'john-doe'
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ❌ FLAKY: Order-dependent tests
|
|
135
|
+
describe('Flaky: Order Dependent', () => {
|
|
136
|
+
let sharedState = [];
|
|
137
|
+
|
|
138
|
+
it('adds item', () => {
|
|
139
|
+
sharedState.push('item1');
|
|
140
|
+
expect(sharedState).toHaveLength(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('has two items', () => {
|
|
144
|
+
// PROBLEM: Depends on previous test running first
|
|
145
|
+
sharedState.push('item2');
|
|
146
|
+
expect(sharedState).toHaveLength(2);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ✅ FIXED: Independent tests
|
|
151
|
+
describe('Fixed: Independent Tests', () => {
|
|
152
|
+
let sharedState: string[];
|
|
153
|
+
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
sharedState = []; // Fresh state each test
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('adds first item', () => {
|
|
159
|
+
sharedState.push('item1');
|
|
160
|
+
expect(sharedState).toHaveLength(1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('adds second item', () => {
|
|
164
|
+
sharedState.push('item2');
|
|
165
|
+
expect(sharedState).toHaveLength(1); // Independent!
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 3. Testing Implementation Details
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
/**
|
|
174
|
+
* ANTI-PATTERN: Testing Implementation
|
|
175
|
+
* Tests break when implementation changes, even if behavior is correct
|
|
176
|
+
*/
|
|
177
|
+
|
|
178
|
+
// ❌ BAD: Testing internal state
|
|
179
|
+
describe('Anti-pattern: Internal State', () => {
|
|
180
|
+
it('sets internal counter', () => {
|
|
181
|
+
const counter = new Counter();
|
|
182
|
+
counter.increment();
|
|
183
|
+
|
|
184
|
+
// PROBLEM: Testing private implementation
|
|
185
|
+
expect(counter._count).toBe(1);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ✅ GOOD: Testing behavior
|
|
190
|
+
describe('Pattern: Test Behavior', () => {
|
|
191
|
+
it('increments and returns count', () => {
|
|
192
|
+
const counter = new Counter();
|
|
193
|
+
|
|
194
|
+
expect(counter.increment()).toBe(1);
|
|
195
|
+
expect(counter.getValue()).toBe(1);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ❌ BAD: Testing function calls
|
|
200
|
+
describe('Anti-pattern: Spy Everything', () => {
|
|
201
|
+
it('calls internal methods', () => {
|
|
202
|
+
const service = new UserService();
|
|
203
|
+
const validateSpy = vi.spyOn(service, 'validateEmail');
|
|
204
|
+
const hashSpy = vi.spyOn(service, 'hashPassword');
|
|
205
|
+
|
|
206
|
+
service.createUser({ email: 'test@test.com', password: '123' });
|
|
207
|
+
|
|
208
|
+
// PROBLEM: Tightly coupled to implementation
|
|
209
|
+
expect(validateSpy).toHaveBeenCalledWith('test@test.com');
|
|
210
|
+
expect(hashSpy).toHaveBeenCalledWith('123');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ✅ GOOD: Testing outcomes
|
|
215
|
+
describe('Pattern: Test Outcomes', () => {
|
|
216
|
+
it('creates user with hashed password', async () => {
|
|
217
|
+
const service = new UserService();
|
|
218
|
+
|
|
219
|
+
const user = await service.createUser({
|
|
220
|
+
email: 'test@test.com',
|
|
221
|
+
password: '123'
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Test the result, not how we got there
|
|
225
|
+
expect(user.email).toBe('test@test.com');
|
|
226
|
+
expect(user.password).not.toBe('123'); // Hashed
|
|
227
|
+
expect(user.password).toMatch(/^\$2[aby]?\$/); // bcrypt format
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ❌ BAD: Testing component internals (React)
|
|
232
|
+
describe('Anti-pattern: Component Internals', () => {
|
|
233
|
+
it('sets state on click', () => {
|
|
234
|
+
const wrapper = shallow(<Counter />);
|
|
235
|
+
|
|
236
|
+
wrapper.find('button').simulate('click');
|
|
237
|
+
|
|
238
|
+
// PROBLEM: Testing React implementation
|
|
239
|
+
expect(wrapper.state('count')).toBe(1);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ✅ GOOD: Testing user-facing behavior
|
|
244
|
+
describe('Pattern: User Behavior', () => {
|
|
245
|
+
it('shows incremented count on click', () => {
|
|
246
|
+
render(<Counter />);
|
|
247
|
+
|
|
248
|
+
const button = screen.getByRole('button', { name: /increment/i });
|
|
249
|
+
fireEvent.click(button);
|
|
250
|
+
|
|
251
|
+
// Test what user sees
|
|
252
|
+
expect(screen.getByText('Count: 1')).toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 4. Over-Mocking
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
/**
|
|
261
|
+
* ANTI-PATTERN: Over-Mocking
|
|
262
|
+
* Mocking so much that tests pass but bugs slip through
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
// ❌ BAD: Everything is mocked
|
|
266
|
+
describe('Anti-pattern: Mock Everything', () => {
|
|
267
|
+
it('processes order', async () => {
|
|
268
|
+
const mockDb = { save: vi.fn() };
|
|
269
|
+
const mockPayment = { charge: vi.fn().mockResolvedValue({ success: true }) };
|
|
270
|
+
const mockEmail = { send: vi.fn() };
|
|
271
|
+
const mockInventory = { reserve: vi.fn().mockResolvedValue(true) };
|
|
272
|
+
const mockShipping = { calculate: vi.fn().mockReturnValue(5.99) };
|
|
273
|
+
|
|
274
|
+
const service = new OrderService({
|
|
275
|
+
db: mockDb,
|
|
276
|
+
payment: mockPayment,
|
|
277
|
+
email: mockEmail,
|
|
278
|
+
inventory: mockInventory,
|
|
279
|
+
shipping: mockShipping
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await service.processOrder(mockOrder);
|
|
283
|
+
|
|
284
|
+
// PROBLEM: We're only testing that our mocks were called
|
|
285
|
+
// The actual integration could be completely broken
|
|
286
|
+
expect(mockPayment.charge).toHaveBeenCalled();
|
|
287
|
+
expect(mockDb.save).toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ✅ GOOD: Mock boundaries, not internals
|
|
292
|
+
describe('Pattern: Mock External Boundaries', () => {
|
|
293
|
+
let service: OrderService;
|
|
294
|
+
let testDb: TestDatabase;
|
|
295
|
+
|
|
296
|
+
beforeAll(async () => {
|
|
297
|
+
testDb = await createTestDatabase();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
beforeEach(async () => {
|
|
301
|
+
// Use real database, mock only external services
|
|
302
|
+
service = new OrderService({
|
|
303
|
+
db: testDb,
|
|
304
|
+
payment: createMockPaymentProvider(), // External API
|
|
305
|
+
email: createMockEmailProvider(), // External service
|
|
306
|
+
inventory: new InventoryService(testDb), // Real, uses test DB
|
|
307
|
+
shipping: new ShippingCalculator() // Real, pure logic
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('processes order end-to-end', async () => {
|
|
312
|
+
const order = await service.processOrder({
|
|
313
|
+
items: [{ productId: 'prod-1', quantity: 2 }],
|
|
314
|
+
customer: testCustomer
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Real assertions on real behavior
|
|
318
|
+
expect(order.status).toBe('confirmed');
|
|
319
|
+
expect(order.total).toBe(29.98);
|
|
320
|
+
|
|
321
|
+
// Verify real database state
|
|
322
|
+
const dbOrder = await testDb.orders.findById(order.id);
|
|
323
|
+
expect(dbOrder.status).toBe('confirmed');
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// The Mock Boundary Principle
|
|
328
|
+
const mockBoundaryGuidelines = {
|
|
329
|
+
mock: [
|
|
330
|
+
'External APIs (payment, email, SMS)',
|
|
331
|
+
'Third-party services',
|
|
332
|
+
'System clock (for determinism)',
|
|
333
|
+
'Random number generators',
|
|
334
|
+
'Network requests'
|
|
335
|
+
],
|
|
336
|
+
dontMock: [
|
|
337
|
+
'Your own code (usually)',
|
|
338
|
+
'Pure functions',
|
|
339
|
+
'Data transformations',
|
|
340
|
+
'Business logic',
|
|
341
|
+
'Internal services (use test doubles with real behavior)'
|
|
342
|
+
]
|
|
343
|
+
};
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### 5. Slow Tests
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
/**
|
|
350
|
+
* ANTI-PATTERN: Slow Tests
|
|
351
|
+
* Tests that take too long, hurting development velocity
|
|
352
|
+
*/
|
|
353
|
+
|
|
354
|
+
// ❌ SLOW: Unnecessary database for unit test
|
|
355
|
+
describe('Anti-pattern: Wrong Layer', () => {
|
|
356
|
+
it('validates email format', async () => {
|
|
357
|
+
// PROBLEM: Spinning up DB just to test string validation
|
|
358
|
+
const db = await createDatabase();
|
|
359
|
+
const service = new UserService(db);
|
|
360
|
+
|
|
361
|
+
const result = service.validateEmail('invalid');
|
|
362
|
+
|
|
363
|
+
expect(result).toBe(false);
|
|
364
|
+
await db.close();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ✅ FAST: Test pure logic directly
|
|
369
|
+
describe('Pattern: Right Layer', () => {
|
|
370
|
+
it('validates email format', () => {
|
|
371
|
+
expect(validateEmail('invalid')).toBe(false);
|
|
372
|
+
expect(validateEmail('valid@example.com')).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// ❌ SLOW: Redundant setup
|
|
377
|
+
describe('Anti-pattern: Heavy Setup', () => {
|
|
378
|
+
it('test 1', async () => {
|
|
379
|
+
const db = await createDatabase();
|
|
380
|
+
await seedUsers(100);
|
|
381
|
+
await seedProducts(1000);
|
|
382
|
+
await seedOrders(500);
|
|
383
|
+
|
|
384
|
+
const result = await getUser('user-1');
|
|
385
|
+
expect(result.name).toBe('User 1');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('test 2', async () => {
|
|
389
|
+
// Same heavy setup repeated!
|
|
390
|
+
const db = await createDatabase();
|
|
391
|
+
await seedUsers(100);
|
|
392
|
+
await seedProducts(1000);
|
|
393
|
+
await seedOrders(500);
|
|
394
|
+
|
|
395
|
+
const result = await getUser('user-2');
|
|
396
|
+
expect(result.name).toBe('User 2');
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// ✅ FAST: Shared setup, minimal data
|
|
401
|
+
describe('Pattern: Efficient Setup', () => {
|
|
402
|
+
let db: TestDatabase;
|
|
403
|
+
|
|
404
|
+
beforeAll(async () => {
|
|
405
|
+
db = await createDatabase();
|
|
406
|
+
// Seed only what's needed for this suite
|
|
407
|
+
await seedUsers(3);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
afterAll(async () => {
|
|
411
|
+
await db.close();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('gets user 1', async () => {
|
|
415
|
+
const result = await getUser('user-1');
|
|
416
|
+
expect(result.name).toBe('User 1');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('gets user 2', async () => {
|
|
420
|
+
const result = await getUser('user-2');
|
|
421
|
+
expect(result.name).toBe('User 2');
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// ❌ SLOW: Real network in unit tests
|
|
426
|
+
describe('Anti-pattern: Real Network', () => {
|
|
427
|
+
it('fetches user from API', async () => {
|
|
428
|
+
// PROBLEM: Hits real API, slow and flaky
|
|
429
|
+
const response = await fetch('https://api.example.com/users/1');
|
|
430
|
+
const user = await response.json();
|
|
431
|
+
|
|
432
|
+
expect(user.name).toBe('John');
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// ✅ FAST: Mock network
|
|
437
|
+
describe('Pattern: Mock Network', () => {
|
|
438
|
+
beforeEach(() => {
|
|
439
|
+
fetchMock.mockResponse(JSON.stringify({ name: 'John' }));
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('fetches user from API', async () => {
|
|
443
|
+
const user = await fetchUser(1);
|
|
444
|
+
expect(user.name).toBe('John');
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### 6. Poor Test Design
|
|
450
|
+
|
|
20
451
|
```typescript
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
452
|
+
/**
|
|
453
|
+
* ANTI-PATTERN: Poor Test Design
|
|
454
|
+
* Tests that are hard to understand or maintain
|
|
455
|
+
*/
|
|
456
|
+
|
|
457
|
+
// ❌ BAD: Giant test with many assertions
|
|
458
|
+
describe('Anti-pattern: Giant Test', () => {
|
|
459
|
+
it('does everything', async () => {
|
|
460
|
+
const service = new OrderService();
|
|
461
|
+
|
|
462
|
+
// Create user
|
|
463
|
+
const user = await service.createUser({ name: 'John' });
|
|
464
|
+
expect(user.id).toBeDefined();
|
|
465
|
+
|
|
466
|
+
// Add payment method
|
|
467
|
+
await service.addPaymentMethod(user.id, { type: 'card' });
|
|
468
|
+
expect(user.paymentMethods).toHaveLength(1);
|
|
469
|
+
|
|
470
|
+
// Create order
|
|
471
|
+
const order = await service.createOrder(user.id, [{ id: 'p1' }]);
|
|
472
|
+
expect(order.status).toBe('pending');
|
|
473
|
+
|
|
474
|
+
// Process payment
|
|
475
|
+
await service.processPayment(order.id);
|
|
476
|
+
expect(order.status).toBe('paid');
|
|
477
|
+
|
|
478
|
+
// Ship order
|
|
479
|
+
await service.shipOrder(order.id);
|
|
480
|
+
expect(order.status).toBe('shipped');
|
|
481
|
+
|
|
482
|
+
// ... 20 more steps
|
|
483
|
+
// PROBLEM: One failure, no idea where
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// ✅ GOOD: Focused tests
|
|
488
|
+
describe('Pattern: Focused Tests', () => {
|
|
489
|
+
describe('Order Creation', () => {
|
|
490
|
+
it('creates pending order from cart items', async () => {
|
|
491
|
+
const order = await service.createOrder(user.id, cartItems);
|
|
492
|
+
|
|
493
|
+
expect(order.status).toBe('pending');
|
|
494
|
+
expect(order.items).toEqual(cartItems);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
describe('Payment Processing', () => {
|
|
499
|
+
it('marks order as paid after successful charge', async () => {
|
|
500
|
+
const order = await createPendingOrder();
|
|
501
|
+
|
|
502
|
+
await service.processPayment(order.id);
|
|
503
|
+
|
|
504
|
+
expect(await getOrder(order.id).status).toBe('paid');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('keeps order pending if charge fails', async () => {
|
|
508
|
+
const order = await createPendingOrder();
|
|
509
|
+
mockPayment.failNextCharge();
|
|
24
510
|
|
|
25
|
-
|
|
26
|
-
|
|
511
|
+
await expect(
|
|
512
|
+
service.processPayment(order.id)
|
|
513
|
+
).rejects.toThrow(PaymentError);
|
|
514
|
+
|
|
515
|
+
expect(await getOrder(order.id).status).toBe('pending');
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// ❌ BAD: Magic values
|
|
521
|
+
describe('Anti-pattern: Magic Values', () => {
|
|
522
|
+
it('calculates discount', () => {
|
|
523
|
+
// PROBLEM: Why 150? Why 15?
|
|
524
|
+
expect(calculateDiscount(150)).toBe(15);
|
|
525
|
+
expect(calculateDiscount(99)).toBe(0);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// ✅ GOOD: Self-documenting values
|
|
530
|
+
describe('Pattern: Clear Intent', () => {
|
|
531
|
+
it('applies 10% discount for orders over $100', () => {
|
|
532
|
+
const DISCOUNT_THRESHOLD = 100;
|
|
533
|
+
const DISCOUNT_RATE = 0.10;
|
|
534
|
+
|
|
535
|
+
const orderAboveThreshold = 150;
|
|
536
|
+
const orderBelowThreshold = 99;
|
|
537
|
+
|
|
538
|
+
expect(calculateDiscount(orderAboveThreshold))
|
|
539
|
+
.toBe(orderAboveThreshold * DISCOUNT_RATE);
|
|
540
|
+
|
|
541
|
+
expect(calculateDiscount(orderBelowThreshold))
|
|
542
|
+
.toBe(0);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// ❌ BAD: Unclear test names
|
|
547
|
+
describe('Anti-pattern: Bad Names', () => {
|
|
548
|
+
it('test1', () => { /* ... */ });
|
|
549
|
+
it('should work', () => { /* ... */ });
|
|
550
|
+
it('handles edge case', () => { /* ... */ });
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// ✅ GOOD: Behavior-describing names
|
|
554
|
+
describe('Pattern: Descriptive Names', () => {
|
|
555
|
+
it('returns empty array when no products match filter', () => { /* ... */ });
|
|
556
|
+
it('throws InvalidEmailError for malformed email addresses', () => { /* ... */ });
|
|
557
|
+
it('retries up to 3 times before failing', () => { /* ... */ });
|
|
558
|
+
});
|
|
27
559
|
```
|
|
28
560
|
|
|
29
|
-
###
|
|
561
|
+
### 7. Test Smells Detection
|
|
562
|
+
|
|
30
563
|
```typescript
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
564
|
+
/**
|
|
565
|
+
* Automated test smell detection
|
|
566
|
+
*/
|
|
567
|
+
|
|
568
|
+
interface TestSmell {
|
|
569
|
+
type: string;
|
|
570
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
571
|
+
pattern: RegExp;
|
|
572
|
+
fix: string;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const testSmells: TestSmell[] = [
|
|
576
|
+
{
|
|
577
|
+
type: 'sleep-in-test',
|
|
578
|
+
severity: 'critical',
|
|
579
|
+
pattern: /await\s+sleep\s*\(/,
|
|
580
|
+
fix: 'Use waitFor or proper async handling'
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
type: 'implementation-testing',
|
|
584
|
+
severity: 'high',
|
|
585
|
+
pattern: /\._\w+|\.state\(|\.instance\(\)/,
|
|
586
|
+
fix: 'Test behavior through public interface'
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
type: 'broad-assertion',
|
|
590
|
+
severity: 'medium',
|
|
591
|
+
pattern: /expect\([^)]+\)\.toBeDefined\(\)/,
|
|
592
|
+
fix: 'Use specific assertions'
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
type: 'magic-number',
|
|
596
|
+
severity: 'medium',
|
|
597
|
+
pattern: /expect\([^)]+\)\.toBe\(\d{3,}\)/,
|
|
598
|
+
fix: 'Use named constants'
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
type: 'commented-test',
|
|
602
|
+
severity: 'low',
|
|
603
|
+
pattern: /\/\/\s*it\(|\/\*[\s\S]*it\(/,
|
|
604
|
+
fix: 'Remove or fix commented tests'
|
|
605
|
+
}
|
|
606
|
+
];
|
|
607
|
+
|
|
608
|
+
function detectTestSmells(testCode: string): DetectedSmell[] {
|
|
609
|
+
const smells: DetectedSmell[] = [];
|
|
35
610
|
|
|
36
|
-
|
|
37
|
-
|
|
611
|
+
for (const smell of testSmells) {
|
|
612
|
+
const matches = testCode.matchAll(new RegExp(smell.pattern, 'g'));
|
|
613
|
+
for (const match of matches) {
|
|
614
|
+
smells.push({
|
|
615
|
+
...smell,
|
|
616
|
+
location: match.index,
|
|
617
|
+
snippet: match[0]
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return smells;
|
|
623
|
+
}
|
|
38
624
|
```
|
|
39
625
|
|
|
40
|
-
|
|
626
|
+
## Use Cases
|
|
627
|
+
|
|
628
|
+
### Refactoring a Flaky Test Suite
|
|
629
|
+
|
|
41
630
|
```typescript
|
|
42
|
-
//
|
|
43
|
-
|
|
631
|
+
// Before: Flaky, slow, hard to maintain
|
|
632
|
+
describe('OrderService (before)', () => {
|
|
633
|
+
it('processes order', async () => {
|
|
634
|
+
const db = await connectToRealDatabase();
|
|
635
|
+
const order = await createOrder(db);
|
|
636
|
+
|
|
637
|
+
// Flaky: timing
|
|
638
|
+
await sleep(500);
|
|
639
|
+
|
|
640
|
+
// Implementation detail
|
|
641
|
+
expect(order._internalState).toBe('processing');
|
|
642
|
+
|
|
643
|
+
// Magic value
|
|
644
|
+
expect(order.total).toBe(127.45);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// After: Reliable, fast, clear
|
|
649
|
+
describe('OrderService (after)', () => {
|
|
650
|
+
let testDb: TestDatabase;
|
|
651
|
+
let service: OrderService;
|
|
652
|
+
|
|
653
|
+
beforeAll(async () => {
|
|
654
|
+
testDb = await createTestDatabase();
|
|
655
|
+
service = new OrderService(testDb);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('calculates total from items', async () => {
|
|
659
|
+
const items = [
|
|
660
|
+
createTestItem({ price: 99.99 }),
|
|
661
|
+
createTestItem({ price: 27.46 })
|
|
662
|
+
];
|
|
663
|
+
|
|
664
|
+
const order = await service.createOrder(items);
|
|
665
|
+
|
|
666
|
+
// Clear expectation
|
|
667
|
+
expect(order.total).toBe(127.45);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('transitions to processing after payment', async () => {
|
|
671
|
+
const order = await createPendingOrder();
|
|
672
|
+
|
|
673
|
+
await service.processPayment(order.id);
|
|
674
|
+
|
|
675
|
+
// Test behavior, not implementation
|
|
676
|
+
await waitFor(() => {
|
|
677
|
+
expect(service.getOrderStatus(order.id)).toBe('processing');
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
});
|
|
44
681
|
```
|
|
45
682
|
|
|
46
|
-
##
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
683
|
+
## Best Practices
|
|
684
|
+
|
|
685
|
+
### Do's
|
|
686
|
+
|
|
687
|
+
- **Test behavior, not implementation** - what it does, not how
|
|
688
|
+
- **Use factories for test data** - consistent, typed data
|
|
689
|
+
- **Write descriptive test names** - document the behavior
|
|
690
|
+
- **Keep tests independent** - no shared mutable state
|
|
691
|
+
- **Mock only external boundaries** - APIs, not your code
|
|
692
|
+
- **Use proper async patterns** - waitFor, not sleep
|
|
693
|
+
- **Make assertions specific** - not just toBeDefined
|
|
694
|
+
- **Run tests in random order** - catch hidden dependencies
|
|
695
|
+
|
|
696
|
+
### Don'ts
|
|
697
|
+
|
|
698
|
+
- Don't use sleep/setTimeout in tests
|
|
699
|
+
- Don't access private properties
|
|
700
|
+
- Don't share state between tests
|
|
701
|
+
- Don't mock everything
|
|
702
|
+
- Don't write 1000-line test files
|
|
703
|
+
- Don't use real network/databases in unit tests
|
|
704
|
+
- Don't ignore flaky tests
|
|
705
|
+
- Don't comment out failing tests
|
|
706
|
+
|
|
707
|
+
## References
|
|
708
|
+
|
|
709
|
+
- [Testing Library Guiding Principles](https://testing-library.com/docs/guiding-principles)
|
|
710
|
+
- [Test Desiderata - Kent Beck](https://medium.com/@kentbeck_7670/test-desiderata-94150638a4b3)
|
|
711
|
+
- [xUnit Test Patterns](http://xunitpatterns.com/)
|
|
712
|
+
- [Growing Object-Oriented Software, Guided by Tests](http://www.growing-object-oriented-software.com/)
|