omgkit 2.2.0 → 2.3.1
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 +3 -3
- package/package.json +1 -1
- package/plugin/skills/databases/database-management/SKILL.md +288 -0
- package/plugin/skills/databases/database-migration/SKILL.md +285 -0
- package/plugin/skills/databases/database-schema-design/SKILL.md +195 -0
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/databases/supabase/SKILL.md +283 -0
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,712 +1,103 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: testing-anti-patterns
|
|
3
|
-
description:
|
|
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
|
|
2
|
+
name: avoiding-testing-anti-patterns
|
|
3
|
+
description: AI agent identifies and fixes common testing anti-patterns that lead to flaky, slow, or unmaintainable test suites. Use when reviewing tests, debugging test failures, or improving test quality.
|
|
13
4
|
---
|
|
14
5
|
|
|
15
|
-
# Testing Anti-Patterns
|
|
6
|
+
# Avoiding Testing Anti-Patterns
|
|
16
7
|
|
|
17
|
-
|
|
8
|
+
## Quick Start
|
|
18
9
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
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
|
|
10
|
+
1. **Identify** - Recognize anti-pattern category (flaky, implementation, over-mocking)
|
|
11
|
+
2. **Assess Severity** - Critical (fix now), High (fix soon), Medium (plan to fix)
|
|
12
|
+
3. **Apply Fix** - Use proper async handling, test behavior not implementation
|
|
13
|
+
4. **Verify** - Run tests in random order, ensure independence
|
|
14
|
+
5. **Prevent** - Add test smell detection to CI
|
|
30
15
|
|
|
31
16
|
## Features
|
|
32
17
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
└─────────────────────────────────────────────────────────────────────────┘
|
|
67
|
-
```
|
|
18
|
+
| Feature | Description | Guide |
|
|
19
|
+
|---------|-------------|-------|
|
|
20
|
+
| Flaky Tests | Random failures destroying trust | Use waitFor, not sleep; deterministic data |
|
|
21
|
+
| Implementation Testing | Breaks on every refactor | Test behavior through public interface |
|
|
22
|
+
| Over-Mocking | Tests pass but bugs slip through | Mock boundaries only, not your own code |
|
|
23
|
+
| Slow Tests | Hurt development velocity | Right test layer, shared setup, mock network |
|
|
24
|
+
| Test Interdependence | Can't run tests in isolation | Fresh state in beforeEach, no shared mutation |
|
|
25
|
+
| Poor Design | Hard to understand/maintain | Descriptive names, focused tests, clear values |
|
|
68
26
|
|
|
69
|
-
|
|
27
|
+
## Common Patterns
|
|
70
28
|
|
|
71
29
|
```typescript
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
});
|
|
30
|
+
// FLAKY: Timing-dependent
|
|
31
|
+
await sleep(100); // May not be enough
|
|
32
|
+
expect(result).toBe('processed');
|
|
133
33
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
34
|
+
// FIXED: Wait for condition
|
|
35
|
+
await waitFor(() => {
|
|
36
|
+
expect(result).toBe('processed');
|
|
37
|
+
}, { timeout: 5000 });
|
|
137
38
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
39
|
+
// FLAKY: Shared state between tests
|
|
40
|
+
let sharedState = [];
|
|
41
|
+
it('test1', () => { sharedState.push('a'); });
|
|
42
|
+
it('test2', () => { expect(sharedState).toHaveLength(1); }); // Order-dependent!
|
|
142
43
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
sharedState.push('item2');
|
|
146
|
-
expect(sharedState).toHaveLength(2);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
44
|
+
// FIXED: Fresh state each test
|
|
45
|
+
beforeEach(() => { sharedState = []; });
|
|
149
46
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
let sharedState: string[];
|
|
47
|
+
// IMPLEMENTATION: Testing private state
|
|
48
|
+
expect(counter._count).toBe(1);
|
|
153
49
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
});
|
|
50
|
+
// BEHAVIOR: Testing public interface
|
|
51
|
+
expect(counter.getValue()).toBe(1);
|
|
157
52
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
53
|
+
// OVER-MOCKING: Everything mocked
|
|
54
|
+
const mockDb = { save: jest.fn() };
|
|
55
|
+
const mockPayment = { charge: jest.fn() };
|
|
56
|
+
// Only testing that mocks were called
|
|
162
57
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
});
|
|
167
|
-
});
|
|
58
|
+
// FIXED: Mock boundaries only
|
|
59
|
+
const testDb = await createTestDatabase(); // Real
|
|
60
|
+
const mockPayment = createMockPaymentProvider(); // External only
|
|
168
61
|
```
|
|
169
62
|
|
|
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
63
|
```
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
451
|
-
```typescript
|
|
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();
|
|
510
|
-
|
|
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
|
-
});
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
### 7. Test Smells Detection
|
|
562
|
-
|
|
563
|
-
```typescript
|
|
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[] = [];
|
|
610
|
-
|
|
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
|
-
}
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
## Use Cases
|
|
627
|
-
|
|
628
|
-
### Refactoring a Flaky Test Suite
|
|
629
|
-
|
|
630
|
-
```typescript
|
|
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
|
-
});
|
|
64
|
+
# Anti-Pattern Severity Guide
|
|
65
|
+
CRITICAL (fix immediately):
|
|
66
|
+
- Flaky tests - Random failures destroy trust
|
|
67
|
+
- Testing implementation - Breaks on every refactor
|
|
68
|
+
- Hidden dependencies - Tests fail mysteriously
|
|
69
|
+
|
|
70
|
+
HIGH (fix soon):
|
|
71
|
+
- Slow tests - Hurt development velocity
|
|
72
|
+
- Test interdependence - Can't run in isolation
|
|
73
|
+
- Over-mocking - Tests pass but bugs slip through
|
|
74
|
+
|
|
75
|
+
MEDIUM (plan to fix):
|
|
76
|
+
- Poor naming - Tests don't document behavior
|
|
77
|
+
- Magic values - Unclear expected values
|
|
78
|
+
- Giant tests - Hard to understand
|
|
79
|
+
|
|
80
|
+
LOW (fix when touching):
|
|
81
|
+
- Commented tests - Remove or fix
|
|
82
|
+
- Duplicate tests - Consolidate
|
|
681
83
|
```
|
|
682
84
|
|
|
683
85
|
## Best Practices
|
|
684
86
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
-
|
|
699
|
-
-
|
|
700
|
-
-
|
|
701
|
-
-
|
|
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/)
|
|
87
|
+
| Do | Avoid |
|
|
88
|
+
|----|-------|
|
|
89
|
+
| Test behavior, not implementation | Accessing private properties |
|
|
90
|
+
| Use factories for test data | Random/inconsistent test data |
|
|
91
|
+
| Write descriptive test names | Generic names like "test1", "should work" |
|
|
92
|
+
| Keep tests independent | Shared mutable state between tests |
|
|
93
|
+
| Mock only external boundaries | Mocking your own code extensively |
|
|
94
|
+
| Use waitFor, not sleep | setTimeout/sleep in tests |
|
|
95
|
+
| Make assertions specific | toBeDefined() for everything |
|
|
96
|
+
| Run tests in random order | Assuming test execution order |
|
|
97
|
+
|
|
98
|
+
## Related Skills
|
|
99
|
+
|
|
100
|
+
- `developing-test-driven` - TDD with proper patterns
|
|
101
|
+
- `testing-with-vitest` - Vitest testing framework
|
|
102
|
+
- `testing-with-playwright` - E2E testing patterns
|
|
103
|
+
- `debugging-systematically` - Debug flaky test failures
|