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,794 +1,133 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: test-driven
|
|
3
|
-
description:
|
|
4
|
-
category: methodology
|
|
5
|
-
triggers:
|
|
6
|
-
- tdd
|
|
7
|
-
- test driven
|
|
8
|
-
- test first
|
|
9
|
-
- red green refactor
|
|
10
|
-
- write tests first
|
|
11
|
-
- testing methodology
|
|
2
|
+
name: developing-test-driven
|
|
3
|
+
description: AI agent practices test-first development with the Red-Green-Refactor cycle for confident, well-designed code. Use when implementing features, fixing bugs, or establishing testing practices.
|
|
12
4
|
---
|
|
13
5
|
|
|
14
|
-
# Test-Driven
|
|
6
|
+
# Developing Test-Driven
|
|
15
7
|
|
|
16
|
-
|
|
8
|
+
## Quick Start
|
|
17
9
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- Write failing tests before implementation
|
|
23
|
-
- Design interfaces through test usage
|
|
24
|
-
- Achieve comprehensive coverage naturally
|
|
25
|
-
- Enable fearless refactoring
|
|
26
|
-
- Document behavior through specs
|
|
27
|
-
- Catch regressions immediately
|
|
28
|
-
- Improve code design through testability
|
|
10
|
+
1. **Red** - Write a failing test that defines desired behavior
|
|
11
|
+
2. **Green** - Write minimal code to make the test pass
|
|
12
|
+
3. **Refactor** - Improve code while keeping tests green
|
|
13
|
+
4. **Repeat** - Continue with next behavior (1-5 minute cycles)
|
|
29
14
|
|
|
30
15
|
## Features
|
|
31
16
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
│ Write a failing test │
|
|
41
|
-
│ - Test doesn't compile OR │
|
|
42
|
-
│ - Test fails with assertion error │
|
|
43
|
-
│ │
|
|
44
|
-
│ │ │
|
|
45
|
-
│ ▼ │
|
|
46
|
-
│ │
|
|
47
|
-
│ 🟢 GREEN │
|
|
48
|
-
│ Make the test pass │
|
|
49
|
-
│ - Write MINIMAL code │
|
|
50
|
-
│ - No more than needed to pass │
|
|
51
|
-
│ - "Fake it till you make it" │
|
|
52
|
-
│ │
|
|
53
|
-
│ │ │
|
|
54
|
-
│ ▼ │
|
|
55
|
-
│ │
|
|
56
|
-
│ 🔄 REFACTOR │
|
|
57
|
-
│ Improve the code │
|
|
58
|
-
│ - Remove duplication │
|
|
59
|
-
│ - Improve naming │
|
|
60
|
-
│ - Extract methods/classes │
|
|
61
|
-
│ - Keep tests green! │
|
|
62
|
-
│ │
|
|
63
|
-
│ │ │
|
|
64
|
-
│ ▼ │
|
|
65
|
-
│ │
|
|
66
|
-
│ Next Test ──────────────────► 🔴 │
|
|
67
|
-
│ │
|
|
68
|
-
└───────────────────────────────────────────────────┘
|
|
69
|
-
|
|
70
|
-
## Cycle Duration
|
|
71
|
-
- Ideal cycle: 1-5 minutes
|
|
72
|
-
- Maximum cycle: 10 minutes
|
|
73
|
-
- If stuck longer: Undo and try smaller step
|
|
74
|
-
```
|
|
17
|
+
| Feature | Description | Guide |
|
|
18
|
+
|---------|-------------|-------|
|
|
19
|
+
| Red-Green-Refactor | Core TDD cycle | Fail -> Pass -> Improve -> Repeat |
|
|
20
|
+
| Test Patterns | Effective test structures | Arrange-Act-Assert, Given-When-Then |
|
|
21
|
+
| Test Fixtures | Reusable test setup | beforeEach, factories, builders |
|
|
22
|
+
| Mocking Strategies | Isolate dependencies | Inject deps, mock boundaries only |
|
|
23
|
+
| Parameterized Tests | Test multiple inputs | `it.each` for input variations |
|
|
24
|
+
| Coverage Analysis | Verify thoroughness | Statements, branches, functions |
|
|
75
25
|
|
|
76
|
-
|
|
26
|
+
## Common Patterns
|
|
77
27
|
|
|
78
28
|
```typescript
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
// ═══════════════════════════════════════════════════════════
|
|
82
|
-
// ITERATION 1: Minimum length requirement
|
|
83
|
-
// ═══════════════════════════════════════════════════════════
|
|
84
|
-
|
|
85
|
-
// 🔴 RED: Write failing test first
|
|
86
|
-
describe('PasswordValidator', () => {
|
|
87
|
-
describe('validate', () => {
|
|
88
|
-
it('returns invalid for passwords shorter than 8 characters', () => {
|
|
89
|
-
const result = validatePassword('short');
|
|
90
|
-
expect(result.valid).toBe(false);
|
|
91
|
-
expect(result.errors).toContain('Password must be at least 8 characters');
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// 🟢 GREEN: Minimal implementation to pass
|
|
97
|
-
interface ValidationResult {
|
|
98
|
-
valid: boolean;
|
|
99
|
-
errors: string[];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function validatePassword(password: string): ValidationResult {
|
|
103
|
-
const errors: string[] = [];
|
|
104
|
-
|
|
105
|
-
if (password.length < 8) {
|
|
106
|
-
errors.push('Password must be at least 8 characters');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
valid: errors.length === 0,
|
|
111
|
-
errors,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 🔄 REFACTOR: Nothing to refactor yet
|
|
116
|
-
|
|
117
|
-
// ═══════════════════════════════════════════════════════════
|
|
118
|
-
// ITERATION 2: Valid password passes
|
|
119
|
-
// ═══════════════════════════════════════════════════════════
|
|
120
|
-
|
|
121
|
-
// 🔴 RED: Add test for valid case
|
|
122
|
-
it('returns valid for password with 8+ characters', () => {
|
|
123
|
-
const result = validatePassword('validpwd');
|
|
124
|
-
expect(result.valid).toBe(true);
|
|
125
|
-
expect(result.errors).toHaveLength(0);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// 🟢 GREEN: Already passes! No changes needed.
|
|
29
|
+
// RED-GREEN-REFACTOR CYCLE
|
|
129
30
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// 🔴 RED: Add uppercase requirement
|
|
135
|
-
it('returns invalid without uppercase letter', () => {
|
|
136
|
-
const result = validatePassword('lowercase123');
|
|
31
|
+
// RED: Write failing test
|
|
32
|
+
it('rejects passwords shorter than 8 characters', () => {
|
|
33
|
+
const result = validatePassword('short');
|
|
137
34
|
expect(result.valid).toBe(false);
|
|
138
|
-
expect(result.errors).toContain('Password must
|
|
35
|
+
expect(result.errors).toContain('Password must be at least 8 characters');
|
|
139
36
|
});
|
|
140
37
|
|
|
141
|
-
//
|
|
142
|
-
function validatePassword(password: string)
|
|
143
|
-
const errors
|
|
144
|
-
|
|
38
|
+
// GREEN: Minimal implementation
|
|
39
|
+
function validatePassword(password: string) {
|
|
40
|
+
const errors = [];
|
|
145
41
|
if (password.length < 8) {
|
|
146
42
|
errors.push('Password must be at least 8 characters');
|
|
147
43
|
}
|
|
148
|
-
|
|
149
|
-
if (!/[A-Z]/.test(password)) {
|
|
150
|
-
errors.push('Password must contain uppercase letter');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
valid: errors.length === 0,
|
|
155
|
-
errors,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// 🔄 REFACTOR: Extract validation rules
|
|
160
|
-
interface ValidationRule {
|
|
161
|
-
test: (password: string) => boolean;
|
|
162
|
-
message: string;
|
|
44
|
+
return { valid: errors.length === 0, errors };
|
|
163
45
|
}
|
|
164
46
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
test: (p) => /[A-Z]/.test(p),
|
|
172
|
-
message: 'Password must contain uppercase letter',
|
|
173
|
-
},
|
|
47
|
+
// REFACTOR: Extract rules (tests stay green)
|
|
48
|
+
const rules = [
|
|
49
|
+
{ test: (p) => p.length >= 8, message: 'Must be 8+ chars' },
|
|
50
|
+
{ test: (p) => /[A-Z]/.test(p), message: 'Must have uppercase' },
|
|
174
51
|
];
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.filter((rule) => !rule.test(password))
|
|
179
|
-
.map((rule) => rule.message);
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
valid: errors.length === 0,
|
|
183
|
-
errors,
|
|
184
|
-
};
|
|
52
|
+
function validatePassword(password: string) {
|
|
53
|
+
const errors = rules.filter(r => !r.test(password)).map(r => r.message);
|
|
54
|
+
return { valid: errors.length === 0, errors };
|
|
185
55
|
}
|
|
186
|
-
|
|
187
|
-
// ═══════════════════════════════════════════════════════════
|
|
188
|
-
// ITERATION 4: More rules become easy to add!
|
|
189
|
-
// ═══════════════════════════════════════════════════════════
|
|
190
|
-
|
|
191
|
-
// 🔴 RED
|
|
192
|
-
it('returns invalid without number', () => {
|
|
193
|
-
const result = validatePassword('NoNumbers!');
|
|
194
|
-
expect(result.valid).toBe(false);
|
|
195
|
-
expect(result.errors).toContain('Password must contain a number');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// 🟢 GREEN: Just add to rules array
|
|
199
|
-
passwordRules.push({
|
|
200
|
-
test: (p) => /\d/.test(p),
|
|
201
|
-
message: 'Password must contain a number',
|
|
202
|
-
});
|
|
203
56
|
```
|
|
204
57
|
|
|
205
|
-
### 3. Test Patterns and Structures
|
|
206
|
-
|
|
207
58
|
```typescript
|
|
208
|
-
// ═══════════════════════════════════════════════════════════
|
|
209
59
|
// ARRANGE-ACT-ASSERT Pattern
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
it('increases total when item is added', () => {
|
|
215
|
-
// ARRANGE: Set up test data and dependencies
|
|
216
|
-
const cart = new ShoppingCart();
|
|
217
|
-
const item = { id: '1', name: 'Widget', price: 10.00 };
|
|
218
|
-
|
|
219
|
-
// ACT: Execute the behavior being tested
|
|
220
|
-
cart.addItem(item);
|
|
221
|
-
|
|
222
|
-
// ASSERT: Verify the expected outcome
|
|
223
|
-
expect(cart.total).toBe(10.00);
|
|
224
|
-
expect(cart.itemCount).toBe(1);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// ═══════════════════════════════════════════════════════════
|
|
230
|
-
// GIVEN-WHEN-THEN Pattern (BDD Style)
|
|
231
|
-
// ═══════════════════════════════════════════════════════════
|
|
232
|
-
|
|
233
|
-
describe('ShoppingCart', () => {
|
|
234
|
-
describe('when applying discount code', () => {
|
|
235
|
-
it('reduces total by discount percentage', () => {
|
|
236
|
-
// GIVEN a cart with items
|
|
237
|
-
const cart = new ShoppingCart();
|
|
238
|
-
cart.addItem({ id: '1', name: 'Widget', price: 100.00 });
|
|
239
|
-
|
|
240
|
-
// WHEN a 20% discount is applied
|
|
241
|
-
cart.applyDiscount('SAVE20');
|
|
242
|
-
|
|
243
|
-
// THEN the total is reduced by 20%
|
|
244
|
-
expect(cart.total).toBe(80.00);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// ═══════════════════════════════════════════════════════════
|
|
250
|
-
// Test Fixture Pattern
|
|
251
|
-
// ═══════════════════════════════════════════════════════════
|
|
252
|
-
|
|
253
|
-
describe('OrderService', () => {
|
|
254
|
-
// Shared fixtures
|
|
255
|
-
let orderService: OrderService;
|
|
256
|
-
let mockPaymentGateway: jest.Mocked<PaymentGateway>;
|
|
257
|
-
let mockInventory: jest.Mocked<InventoryService>;
|
|
258
|
-
|
|
259
|
-
// Common test data
|
|
260
|
-
const validOrder: Order = {
|
|
261
|
-
id: 'order-123',
|
|
262
|
-
items: [{ productId: 'prod-1', quantity: 2, price: 25.00 }],
|
|
263
|
-
total: 50.00,
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
beforeEach(() => {
|
|
267
|
-
// Fresh mocks for each test
|
|
268
|
-
mockPaymentGateway = {
|
|
269
|
-
charge: jest.fn().mockResolvedValue({ success: true }),
|
|
270
|
-
};
|
|
271
|
-
mockInventory = {
|
|
272
|
-
reserve: jest.fn().mockResolvedValue(true),
|
|
273
|
-
release: jest.fn().mockResolvedValue(true),
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
orderService = new OrderService(mockPaymentGateway, mockInventory);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
afterEach(() => {
|
|
280
|
-
jest.clearAllMocks();
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe('submitOrder', () => {
|
|
284
|
-
it('reserves inventory and charges payment', async () => {
|
|
285
|
-
await orderService.submitOrder(validOrder);
|
|
286
|
-
|
|
287
|
-
expect(mockInventory.reserve).toHaveBeenCalledWith(validOrder.items);
|
|
288
|
-
expect(mockPaymentGateway.charge).toHaveBeenCalledWith(50.00);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('releases inventory if payment fails', async () => {
|
|
292
|
-
mockPaymentGateway.charge.mockResolvedValue({ success: false });
|
|
293
|
-
|
|
294
|
-
await expect(orderService.submitOrder(validOrder)).rejects.toThrow();
|
|
295
|
-
|
|
296
|
-
expect(mockInventory.release).toHaveBeenCalledWith(validOrder.items);
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
// ═══════════════════════════════════════════════════════════
|
|
302
|
-
// Parameterized Tests
|
|
303
|
-
// ═══════════════════════════════════════════════════════════
|
|
304
|
-
|
|
305
|
-
describe('EmailValidator', () => {
|
|
306
|
-
// Valid email test cases
|
|
307
|
-
it.each([
|
|
308
|
-
['simple@example.com', 'simple email'],
|
|
309
|
-
['user.name@domain.org', 'email with dots'],
|
|
310
|
-
['user+tag@example.com', 'email with plus'],
|
|
311
|
-
['user@subdomain.domain.com', 'email with subdomain'],
|
|
312
|
-
])('validates %s as valid (%s)', (email, _description) => {
|
|
313
|
-
expect(isValidEmail(email)).toBe(true);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// Invalid email test cases
|
|
317
|
-
it.each([
|
|
318
|
-
['plaintext', 'no @ symbol'],
|
|
319
|
-
['missing@domain', 'no TLD'],
|
|
320
|
-
['@nodomain.com', 'no local part'],
|
|
321
|
-
['spaces in@email.com', 'contains spaces'],
|
|
322
|
-
['', 'empty string'],
|
|
323
|
-
])('rejects %s as invalid (%s)', (email, _description) => {
|
|
324
|
-
expect(isValidEmail(email)).toBe(false);
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// ═══════════════════════════════════════════════════════════
|
|
329
|
-
// Builder Pattern for Test Data
|
|
330
|
-
// ═══════════════════════════════════════════════════════════
|
|
331
|
-
|
|
332
|
-
class UserBuilder {
|
|
333
|
-
private user: Partial<User> = {
|
|
334
|
-
id: 'user-123',
|
|
335
|
-
email: 'test@example.com',
|
|
336
|
-
name: 'Test User',
|
|
337
|
-
role: 'user',
|
|
338
|
-
active: true,
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
withId(id: string): this {
|
|
342
|
-
this.user.id = id;
|
|
343
|
-
return this;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
withEmail(email: string): this {
|
|
347
|
-
this.user.email = email;
|
|
348
|
-
return this;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
withRole(role: UserRole): this {
|
|
352
|
-
this.user.role = role;
|
|
353
|
-
return this;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
inactive(): this {
|
|
357
|
-
this.user.active = false;
|
|
358
|
-
return this;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
admin(): this {
|
|
362
|
-
this.user.role = 'admin';
|
|
363
|
-
return this;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
build(): User {
|
|
367
|
-
return this.user as User;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Usage in tests
|
|
372
|
-
describe('PermissionService', () => {
|
|
373
|
-
it('grants admin access to admin users', () => {
|
|
374
|
-
const admin = new UserBuilder().admin().build();
|
|
375
|
-
const service = new PermissionService();
|
|
376
|
-
|
|
377
|
-
expect(service.canAccessAdminPanel(admin)).toBe(true);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it('denies admin access to regular users', () => {
|
|
381
|
-
const user = new UserBuilder().withRole('user').build();
|
|
382
|
-
const service = new PermissionService();
|
|
383
|
-
|
|
384
|
-
expect(service.canAccessAdminPanel(user)).toBe(false);
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
### 4. Mocking Strategies
|
|
390
|
-
|
|
391
|
-
```typescript
|
|
392
|
-
// ═══════════════════════════════════════════════════════════
|
|
393
|
-
// Dependency Injection for Testability
|
|
394
|
-
// ═══════════════════════════════════════════════════════════
|
|
395
|
-
|
|
396
|
-
// BAD: Hard to test
|
|
397
|
-
class UserService {
|
|
398
|
-
async getUser(id: string) {
|
|
399
|
-
const response = await fetch(`/api/users/${id}`); // Can't mock
|
|
400
|
-
return response.json();
|
|
401
|
-
}
|
|
402
|
-
}
|
|
60
|
+
it('increases total when item added', () => {
|
|
61
|
+
// ARRANGE
|
|
62
|
+
const cart = new ShoppingCart();
|
|
63
|
+
const item = { id: '1', price: 10.00 };
|
|
403
64
|
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
get<T>(url: string): Promise<T>;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
class UserService {
|
|
410
|
-
constructor(private http: HttpClient) {}
|
|
411
|
-
|
|
412
|
-
async getUser(id: string): Promise<User> {
|
|
413
|
-
return this.http.get<User>(`/api/users/${id}`);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Test with mock
|
|
418
|
-
describe('UserService', () => {
|
|
419
|
-
it('fetches user by id', async () => {
|
|
420
|
-
const mockHttp: jest.Mocked<HttpClient> = {
|
|
421
|
-
get: jest.fn().mockResolvedValue({ id: '1', name: 'John' }),
|
|
422
|
-
};
|
|
65
|
+
// ACT
|
|
66
|
+
cart.addItem(item);
|
|
423
67
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
expect(mockHttp.get).toHaveBeenCalledWith('/api/users/1');
|
|
428
|
-
expect(user.name).toBe('John');
|
|
429
|
-
});
|
|
68
|
+
// ASSERT
|
|
69
|
+
expect(cart.total).toBe(10.00);
|
|
430
70
|
});
|
|
431
71
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
warn: () => {},
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
// MOCK: Verifies interactions
|
|
444
|
-
const mockLogger = {
|
|
445
|
-
log: jest.fn(),
|
|
446
|
-
error: jest.fn(),
|
|
447
|
-
warn: jest.fn(),
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
// SPY: Wraps real implementation
|
|
451
|
-
const realLogger = new ConsoleLogger();
|
|
452
|
-
const spyLogger = jest.spyOn(realLogger, 'error');
|
|
453
|
-
|
|
454
|
-
// ═══════════════════════════════════════════════════════════
|
|
455
|
-
// Time-Based Testing
|
|
456
|
-
// ═══════════════════════════════════════════════════════════
|
|
457
|
-
|
|
458
|
-
describe('TokenService', () => {
|
|
459
|
-
beforeEach(() => {
|
|
460
|
-
jest.useFakeTimers();
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
afterEach(() => {
|
|
464
|
-
jest.useRealTimers();
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it('expires tokens after 1 hour', () => {
|
|
468
|
-
const service = new TokenService();
|
|
469
|
-
const token = service.createToken('user-1');
|
|
470
|
-
|
|
471
|
-
// Initially valid
|
|
472
|
-
expect(service.isValid(token)).toBe(true);
|
|
473
|
-
|
|
474
|
-
// Advance time by 59 minutes - still valid
|
|
475
|
-
jest.advanceTimersByTime(59 * 60 * 1000);
|
|
476
|
-
expect(service.isValid(token)).toBe(true);
|
|
477
|
-
|
|
478
|
-
// Advance time by 2 more minutes - now expired
|
|
479
|
-
jest.advanceTimersByTime(2 * 60 * 1000);
|
|
480
|
-
expect(service.isValid(token)).toBe(false);
|
|
481
|
-
});
|
|
72
|
+
// PARAMETERIZED TESTS
|
|
73
|
+
it.each([
|
|
74
|
+
['simple@example.com', true],
|
|
75
|
+
['user.name@domain.org', true],
|
|
76
|
+
['invalid', false],
|
|
77
|
+
['@nodomain.com', false],
|
|
78
|
+
])('validates %s as %s', (email, expected) => {
|
|
79
|
+
expect(isValidEmail(email)).toBe(expected);
|
|
482
80
|
});
|
|
483
81
|
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
// ═══════════════════════════════════════════════════════════
|
|
487
|
-
|
|
488
|
-
describe('NotificationService', () => {
|
|
489
|
-
it('sends notification and waits for delivery', async () => {
|
|
490
|
-
const service = new NotificationService();
|
|
491
|
-
|
|
492
|
-
// Test async success
|
|
493
|
-
const result = await service.send('Hello');
|
|
494
|
-
expect(result.delivered).toBe(true);
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
it('handles async errors', async () => {
|
|
498
|
-
const service = new NotificationService();
|
|
499
|
-
service.setOffline(true);
|
|
500
|
-
|
|
501
|
-
// Test async error
|
|
502
|
-
await expect(service.send('Hello')).rejects.toThrow('Network unavailable');
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
it('retries failed deliveries', async () => {
|
|
506
|
-
const mockTransport = {
|
|
507
|
-
send: jest
|
|
508
|
-
.fn()
|
|
509
|
-
.mockRejectedValueOnce(new Error('Temporary failure'))
|
|
510
|
-
.mockResolvedValueOnce({ success: true }),
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
const service = new NotificationService(mockTransport);
|
|
514
|
-
await service.send('Hello');
|
|
515
|
-
|
|
516
|
-
expect(mockTransport.send).toHaveBeenCalledTimes(2);
|
|
517
|
-
});
|
|
518
|
-
});
|
|
82
|
+
// BUILDER PATTERN for test data
|
|
83
|
+
const user = new UserBuilder().admin().inactive().build();
|
|
519
84
|
```
|
|
520
85
|
|
|
521
|
-
### 5. Coverage and Quality Metrics
|
|
522
|
-
|
|
523
|
-
```markdown
|
|
524
|
-
## Code Coverage Guidelines
|
|
525
|
-
|
|
526
|
-
### Coverage Targets by Type
|
|
527
|
-
|
|
528
|
-
| Test Type | Target | Rationale |
|
|
529
|
-
|-----------|--------|-----------|
|
|
530
|
-
| Unit Tests | 80%+ | Core logic coverage |
|
|
531
|
-
| Integration | 60%+ | Critical paths |
|
|
532
|
-
| E2E | 100% critical | User journeys |
|
|
533
|
-
|
|
534
|
-
### What Coverage Tells You
|
|
535
|
-
✅ Code that IS executed by tests
|
|
536
|
-
❌ Does NOT mean code is well-tested
|
|
537
|
-
❌ Does NOT mean edge cases covered
|
|
538
|
-
❌ Does NOT mean behavior is verified
|
|
539
|
-
|
|
540
|
-
### Meaningful Coverage
|
|
541
|
-
```typescript
|
|
542
|
-
// 100% coverage but NOT well-tested
|
|
543
|
-
function divide(a: number, b: number): number {
|
|
544
|
-
return a / b;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
it('divides two numbers', () => {
|
|
548
|
-
expect(divide(10, 2)).toBe(5); // 100% coverage!
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
// Missing tests:
|
|
552
|
-
// - divide(10, 0) - Division by zero
|
|
553
|
-
// - divide(0, 5) - Zero numerator
|
|
554
|
-
// - divide(-10, 2) - Negative numbers
|
|
555
|
-
// - divide(1.5, 0.3) - Floating point
|
|
556
86
|
```
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
# UserService.ts | 95.00 | 90.00 | 100.00 | 94.74 |
|
|
570
|
-
# OrderService.ts | 89.47 | 80.00 | 90.00 | 89.19 |
|
|
87
|
+
# Mocking Guidelines
|
|
88
|
+
MOCK (external boundaries):
|
|
89
|
+
- External APIs (payment, email)
|
|
90
|
+
- Third-party services
|
|
91
|
+
- System clock, random
|
|
92
|
+
- Network requests
|
|
93
|
+
|
|
94
|
+
DON'T MOCK (your code):
|
|
95
|
+
- Pure functions
|
|
96
|
+
- Data transformations
|
|
97
|
+
- Business logic
|
|
98
|
+
- Internal services
|
|
571
99
|
```
|
|
572
100
|
|
|
573
|
-
### Branch Coverage Focus
|
|
574
|
-
```typescript
|
|
575
|
-
// Branch coverage example
|
|
576
|
-
function processOrder(order: Order): Result {
|
|
577
|
-
if (order.total > 1000) { // Branch 1
|
|
578
|
-
if (order.customerType === 'vip') { // Branch 2
|
|
579
|
-
return applyVipDiscount(order);
|
|
580
|
-
}
|
|
581
|
-
return applyBulkDiscount(order);
|
|
582
|
-
}
|
|
583
|
-
return processStandard(order);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Tests needed for 100% branch coverage:
|
|
587
|
-
// 1. order.total <= 1000
|
|
588
|
-
// 2. order.total > 1000 AND customerType === 'vip'
|
|
589
|
-
// 3. order.total > 1000 AND customerType !== 'vip'
|
|
590
101
|
```
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
// Arrange
|
|
603
|
-
const mockUserRepo: jest.Mocked<UserRepository> = {
|
|
604
|
-
save: jest.fn().mockResolvedValue({ id: '1', email: 'test@test.com' }),
|
|
605
|
-
findByEmail: jest.fn().mockResolvedValue(null),
|
|
606
|
-
};
|
|
607
|
-
const mockEmailService: jest.Mocked<EmailService> = {
|
|
608
|
-
send: jest.fn().mockResolvedValue(true),
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
const useCase = new CreateUserUseCase(mockUserRepo, mockEmailService);
|
|
612
|
-
|
|
613
|
-
// Act
|
|
614
|
-
const result = await useCase.execute({
|
|
615
|
-
email: 'test@test.com',
|
|
616
|
-
password: 'password123',
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
// Assert
|
|
620
|
-
expect(result.success).toBe(true);
|
|
621
|
-
expect(mockUserRepo.save).toHaveBeenCalled();
|
|
622
|
-
expect(mockEmailService.send).toHaveBeenCalledWith(
|
|
623
|
-
'test@test.com',
|
|
624
|
-
expect.stringContaining('Welcome')
|
|
625
|
-
);
|
|
626
|
-
});
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
// 2. Then implement the use case
|
|
630
|
-
class CreateUserUseCase {
|
|
631
|
-
constructor(
|
|
632
|
-
private userRepo: UserRepository,
|
|
633
|
-
private emailService: EmailService
|
|
634
|
-
) {}
|
|
635
|
-
|
|
636
|
-
async execute(input: CreateUserInput): Promise<CreateUserOutput> {
|
|
637
|
-
// Check if user exists
|
|
638
|
-
const existing = await this.userRepo.findByEmail(input.email);
|
|
639
|
-
if (existing) {
|
|
640
|
-
return { success: false, error: 'Email already registered' };
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Create user
|
|
644
|
-
const user = await this.userRepo.save({
|
|
645
|
-
email: input.email,
|
|
646
|
-
passwordHash: await hash(input.password),
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
// Send welcome email
|
|
650
|
-
await this.emailService.send(user.email, 'Welcome to our platform!');
|
|
651
|
-
|
|
652
|
-
return { success: true, userId: user.id };
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// ═══════════════════════════════════════════════════════════
|
|
657
|
-
// TDD with API Endpoints
|
|
658
|
-
// ═══════════════════════════════════════════════════════════
|
|
659
|
-
|
|
660
|
-
describe('POST /api/users', () => {
|
|
661
|
-
it('creates user and returns 201', async () => {
|
|
662
|
-
const response = await request(app)
|
|
663
|
-
.post('/api/users')
|
|
664
|
-
.send({
|
|
665
|
-
email: 'new@example.com',
|
|
666
|
-
password: 'securepass123',
|
|
667
|
-
})
|
|
668
|
-
.expect(201);
|
|
669
|
-
|
|
670
|
-
expect(response.body).toMatchObject({
|
|
671
|
-
id: expect.any(String),
|
|
672
|
-
email: 'new@example.com',
|
|
673
|
-
});
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
it('returns 400 for invalid email', async () => {
|
|
677
|
-
const response = await request(app)
|
|
678
|
-
.post('/api/users')
|
|
679
|
-
.send({
|
|
680
|
-
email: 'not-an-email',
|
|
681
|
-
password: 'securepass123',
|
|
682
|
-
})
|
|
683
|
-
.expect(400);
|
|
684
|
-
|
|
685
|
-
expect(response.body.error).toContain('email');
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
it('returns 409 for duplicate email', async () => {
|
|
689
|
-
// Create user first
|
|
690
|
-
await createUser({ email: 'exists@example.com' });
|
|
691
|
-
|
|
692
|
-
// Try to create duplicate
|
|
693
|
-
const response = await request(app)
|
|
694
|
-
.post('/api/users')
|
|
695
|
-
.send({
|
|
696
|
-
email: 'exists@example.com',
|
|
697
|
-
password: 'securepass123',
|
|
698
|
-
})
|
|
699
|
-
.expect(409);
|
|
700
|
-
|
|
701
|
-
expect(response.body.error).toContain('already exists');
|
|
702
|
-
});
|
|
703
|
-
});
|
|
704
|
-
```
|
|
705
|
-
|
|
706
|
-
## Use Cases
|
|
707
|
-
|
|
708
|
-
### Building a Calculator with TDD
|
|
709
|
-
|
|
710
|
-
```typescript
|
|
711
|
-
// Progressive TDD session building a calculator
|
|
712
|
-
|
|
713
|
-
// Test 1: Addition
|
|
714
|
-
describe('Calculator', () => {
|
|
715
|
-
describe('add', () => {
|
|
716
|
-
it('adds two positive numbers', () => {
|
|
717
|
-
expect(add(2, 3)).toBe(5);
|
|
718
|
-
});
|
|
719
|
-
});
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
// Implementation 1
|
|
723
|
-
function add(a: number, b: number): number {
|
|
724
|
-
return a + b;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Test 2: Negative numbers
|
|
728
|
-
it('adds negative numbers', () => {
|
|
729
|
-
expect(add(-2, -3)).toBe(-5);
|
|
730
|
-
expect(add(-2, 3)).toBe(1);
|
|
731
|
-
});
|
|
732
|
-
// Already passes!
|
|
733
|
-
|
|
734
|
-
// Test 3: Subtraction
|
|
735
|
-
describe('subtract', () => {
|
|
736
|
-
it('subtracts two numbers', () => {
|
|
737
|
-
expect(subtract(5, 3)).toBe(2);
|
|
738
|
-
});
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
// Implementation 3
|
|
742
|
-
function subtract(a: number, b: number): number {
|
|
743
|
-
return a - b;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// Refactor: Extract to class
|
|
747
|
-
class Calculator {
|
|
748
|
-
add(a: number, b: number): number {
|
|
749
|
-
return a + b;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
subtract(a: number, b: number): number {
|
|
753
|
-
return a - b;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Continue with multiply, divide, etc.
|
|
102
|
+
# Coverage Analysis
|
|
103
|
+
| Type | Target | Notes |
|
|
104
|
+
|------|--------|-------|
|
|
105
|
+
| Statements | 80%+ | Code executed |
|
|
106
|
+
| Branches | 70%+ | If/else paths |
|
|
107
|
+
| Functions | 90%+ | All functions called |
|
|
108
|
+
|
|
109
|
+
REMEMBER: 100% coverage != well-tested
|
|
110
|
+
- Test edge cases
|
|
111
|
+
- Test error paths
|
|
112
|
+
- Test boundary conditions
|
|
758
113
|
```
|
|
759
114
|
|
|
760
115
|
## Best Practices
|
|
761
116
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
-
|
|
778
|
-
-
|
|
779
|
-
- Don't mock what you don't own
|
|
780
|
-
- Don't skip the refactor step
|
|
781
|
-
- Don't write tests for trivial code
|
|
782
|
-
- Don't let tests become too slow
|
|
783
|
-
- Don't test framework/library code
|
|
784
|
-
- Don't ignore flaky tests
|
|
785
|
-
- Don't over-mock (test integration too)
|
|
786
|
-
- Don't forget to run all tests before commit
|
|
787
|
-
|
|
788
|
-
## References
|
|
789
|
-
|
|
790
|
-
- [Test Driven Development by Kent Beck](https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530)
|
|
791
|
-
- [Growing Object-Oriented Software, Guided by Tests](http://www.growing-object-oriented-software.com/)
|
|
792
|
-
- [The Art of Unit Testing](https://www.manning.com/books/the-art-of-unit-testing-third-edition)
|
|
793
|
-
- [Jest Documentation](https://jestjs.io/docs/getting-started)
|
|
794
|
-
- [Testing Library](https://testing-library.com/)
|
|
117
|
+
| Do | Avoid |
|
|
118
|
+
|----|-------|
|
|
119
|
+
| Write test BEFORE implementation | Writing implementation first |
|
|
120
|
+
| Keep tests focused on ONE behavior | Testing multiple things in one test |
|
|
121
|
+
| Use descriptive test names | Generic names like "test1" |
|
|
122
|
+
| Run tests frequently (every few minutes) | Long gaps between test runs |
|
|
123
|
+
| Refactor only when tests green | Refactoring with failing tests |
|
|
124
|
+
| Test behavior, not implementation | Testing private methods directly |
|
|
125
|
+
| Keep cycles short (1-5 minutes) | 30+ minute cycles |
|
|
126
|
+
| Mock only external dependencies | Over-mocking your own code |
|
|
127
|
+
|
|
128
|
+
## Related Skills
|
|
129
|
+
|
|
130
|
+
- `avoiding-testing-anti-patterns` - Avoid common test mistakes
|
|
131
|
+
- `testing-with-vitest` - Vitest testing framework
|
|
132
|
+
- `testing-with-playwright` - E2E testing with Playwright
|
|
133
|
+
- `verifying-before-completion` - Ensure test quality
|