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,39 +1,794 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: test-driven-development
|
|
3
|
-
description:
|
|
3
|
+
description: Test-first development with Red-Green-Refactor cycle, behavior-driven specs, and comprehensive testing strategies
|
|
4
|
+
category: methodology
|
|
5
|
+
triggers:
|
|
6
|
+
- tdd
|
|
7
|
+
- test driven
|
|
8
|
+
- test first
|
|
9
|
+
- red green refactor
|
|
10
|
+
- write tests first
|
|
11
|
+
- testing methodology
|
|
4
12
|
---
|
|
5
13
|
|
|
6
|
-
# Test-Driven Development
|
|
14
|
+
# Test-Driven Development
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
1. 🔴 **RED** - Write failing test
|
|
10
|
-
2. 🟢 **GREEN** - Make it pass (minimal code)
|
|
11
|
-
3. ♻️ **REFACTOR** - Improve code
|
|
16
|
+
Master **test-first development** with the Red-Green-Refactor cycle. This skill enables confident code changes through comprehensive test coverage, emergent design, and continuous validation.
|
|
12
17
|
|
|
13
|
-
##
|
|
14
|
-
- No production code without failing test
|
|
15
|
-
- Only enough code to pass test
|
|
16
|
-
- Refactor only when green
|
|
17
|
-
- One assertion per test
|
|
18
|
+
## Purpose
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
Build reliable software through test-first practices:
|
|
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
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
### 1. The Red-Green-Refactor Cycle
|
|
33
|
+
|
|
34
|
+
```markdown
|
|
35
|
+
## TDD Cycle Visualization
|
|
36
|
+
|
|
37
|
+
┌───────────────────────────────────────────────────┐
|
|
38
|
+
│ │
|
|
39
|
+
│ 🔴 RED │
|
|
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
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Writing Effective Tests
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// TDD Example: Building a Password Validator
|
|
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.
|
|
129
|
+
|
|
130
|
+
// ═══════════════════════════════════════════════════════════
|
|
131
|
+
// ITERATION 3: Require uppercase letter
|
|
132
|
+
// ═══════════════════════════════════════════════════════════
|
|
133
|
+
|
|
134
|
+
// 🔴 RED: Add uppercase requirement
|
|
135
|
+
it('returns invalid without uppercase letter', () => {
|
|
136
|
+
const result = validatePassword('lowercase123');
|
|
137
|
+
expect(result.valid).toBe(false);
|
|
138
|
+
expect(result.errors).toContain('Password must contain uppercase letter');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 🟢 GREEN: Add uppercase check
|
|
142
|
+
function validatePassword(password: string): ValidationResult {
|
|
143
|
+
const errors: string[] = [];
|
|
144
|
+
|
|
145
|
+
if (password.length < 8) {
|
|
146
|
+
errors.push('Password must be at least 8 characters');
|
|
147
|
+
}
|
|
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;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const passwordRules: ValidationRule[] = [
|
|
166
|
+
{
|
|
167
|
+
test: (p) => p.length >= 8,
|
|
168
|
+
message: 'Password must be at least 8 characters',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
test: (p) => /[A-Z]/.test(p),
|
|
172
|
+
message: 'Password must contain uppercase letter',
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
function validatePassword(password: string): ValidationResult {
|
|
177
|
+
const errors = passwordRules
|
|
178
|
+
.filter((rule) => !rule.test(password))
|
|
179
|
+
.map((rule) => rule.message);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
valid: errors.length === 0,
|
|
183
|
+
errors,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
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
|
+
```
|
|
204
|
+
|
|
205
|
+
### 3. Test Patterns and Structures
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// ═══════════════════════════════════════════════════════════
|
|
209
|
+
// ARRANGE-ACT-ASSERT Pattern
|
|
210
|
+
// ═══════════════════════════════════════════════════════════
|
|
211
|
+
|
|
212
|
+
describe('ShoppingCart', () => {
|
|
213
|
+
describe('addItem', () => {
|
|
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
|
+
}
|
|
403
|
+
|
|
404
|
+
// GOOD: Dependency injection
|
|
405
|
+
interface HttpClient {
|
|
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
|
+
};
|
|
423
|
+
|
|
424
|
+
const service = new UserService(mockHttp);
|
|
425
|
+
const user = await service.getUser('1');
|
|
426
|
+
|
|
427
|
+
expect(mockHttp.get).toHaveBeenCalledWith('/api/users/1');
|
|
428
|
+
expect(user.name).toBe('John');
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ═══════════════════════════════════════════════════════════
|
|
433
|
+
// Stub vs Mock vs Spy
|
|
434
|
+
// ═══════════════════════════════════════════════════════════
|
|
435
|
+
|
|
436
|
+
// STUB: Provides canned responses
|
|
437
|
+
const stubLogger: Logger = {
|
|
438
|
+
log: () => {},
|
|
439
|
+
error: () => {},
|
|
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
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// ═══════════════════════════════════════════════════════════
|
|
485
|
+
// Testing Async Code
|
|
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
|
+
});
|
|
519
|
+
```
|
|
520
|
+
|
|
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
|
+
```
|
|
557
|
+
|
|
558
|
+
### Coverage Report Analysis
|
|
559
|
+
```bash
|
|
560
|
+
# Generate coverage report
|
|
561
|
+
npm test -- --coverage
|
|
562
|
+
|
|
563
|
+
# Example output:
|
|
564
|
+
# -------------------|---------|----------|---------|---------|
|
|
565
|
+
# File | % Stmts | % Branch | % Funcs | % Lines |
|
|
566
|
+
# -------------------|---------|----------|---------|---------|
|
|
567
|
+
# All files | 85.32 | 72.41 | 91.23 | 84.76 |
|
|
568
|
+
# src/services | 92.14 | 85.00 | 95.00 | 91.89 |
|
|
569
|
+
# UserService.ts | 95.00 | 90.00 | 100.00 | 94.74 |
|
|
570
|
+
# OrderService.ts | 89.47 | 80.00 | 90.00 | 89.19 |
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Branch Coverage Focus
|
|
20
574
|
```typescript
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
```
|
|
591
|
+
|
|
592
|
+
### 6. TDD with Different Architectures
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
// ═══════════════════════════════════════════════════════════
|
|
596
|
+
// TDD with Clean Architecture
|
|
597
|
+
// ═══════════════════════════════════════════════════════════
|
|
598
|
+
|
|
599
|
+
// 1. Start with USE CASE test
|
|
600
|
+
describe('CreateUserUseCase', () => {
|
|
601
|
+
it('creates user and sends welcome email', async () => {
|
|
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);
|
|
25
738
|
});
|
|
26
739
|
});
|
|
27
740
|
|
|
28
|
-
//
|
|
29
|
-
function
|
|
30
|
-
return
|
|
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
|
+
}
|
|
31
755
|
}
|
|
32
756
|
|
|
33
|
-
//
|
|
757
|
+
// Continue with multiply, divide, etc.
|
|
34
758
|
```
|
|
35
759
|
|
|
36
|
-
##
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
760
|
+
## Best Practices
|
|
761
|
+
|
|
762
|
+
### Do's
|
|
763
|
+
|
|
764
|
+
- Write the test BEFORE the implementation
|
|
765
|
+
- Keep tests focused on ONE behavior
|
|
766
|
+
- Use descriptive test names that document behavior
|
|
767
|
+
- Run tests frequently (every few minutes)
|
|
768
|
+
- Refactor only when tests are green
|
|
769
|
+
- Test behavior, not implementation
|
|
770
|
+
- Keep the Red-Green-Refactor cycle short
|
|
771
|
+
- Use test doubles (mocks) for external dependencies
|
|
772
|
+
- Organize tests to mirror source structure
|
|
773
|
+
- Delete tests that no longer add value
|
|
774
|
+
|
|
775
|
+
### Don'ts
|
|
776
|
+
|
|
777
|
+
- Don't write implementation before tests
|
|
778
|
+
- Don't test private methods directly
|
|
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/)
|