agentic-team-templates 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/bin/cli.js +4 -1
- package/package.json +8 -3
- package/src/index.js +471 -7
- package/src/index.test.js +947 -0
- package/templates/testing/.cursorrules/advanced-techniques.md +596 -0
- package/templates/testing/.cursorrules/ci-cd-integration.md +603 -0
- package/templates/testing/.cursorrules/overview.md +163 -0
- package/templates/testing/.cursorrules/performance-testing.md +536 -0
- package/templates/testing/.cursorrules/quality-metrics.md +456 -0
- package/templates/testing/.cursorrules/reliability.md +557 -0
- package/templates/testing/.cursorrules/tdd-methodology.md +294 -0
- package/templates/testing/.cursorrules/test-data.md +565 -0
- package/templates/testing/.cursorrules/test-design.md +511 -0
- package/templates/testing/.cursorrules/test-types.md +398 -0
- package/templates/testing/CLAUDE.md +1134 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
# Test Design Patterns
|
|
2
|
+
|
|
3
|
+
Guidelines for designing effective, maintainable tests.
|
|
4
|
+
|
|
5
|
+
## Core Patterns
|
|
6
|
+
|
|
7
|
+
### Arrange-Act-Assert (AAA)
|
|
8
|
+
|
|
9
|
+
The fundamental pattern for all tests:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
it('calculates order total with discount', () => {
|
|
13
|
+
// Arrange - Set up test data and dependencies
|
|
14
|
+
const items = [
|
|
15
|
+
{ price: 100, quantity: 2 },
|
|
16
|
+
{ price: 50, quantity: 1 },
|
|
17
|
+
];
|
|
18
|
+
const discount = { type: 'percentage', value: 10 };
|
|
19
|
+
|
|
20
|
+
// Act - Execute the code under test
|
|
21
|
+
const total = calculateOrderTotal(items, discount);
|
|
22
|
+
|
|
23
|
+
// Assert - Verify the result
|
|
24
|
+
expect(total).toBe(225); // (200 + 50) * 0.9
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Rules:**
|
|
29
|
+
- Separate sections with blank lines
|
|
30
|
+
- One "Act" per test
|
|
31
|
+
- Keep "Arrange" minimal (use factories)
|
|
32
|
+
- Assertions should be obvious
|
|
33
|
+
|
|
34
|
+
### Given-When-Then (BDD)
|
|
35
|
+
|
|
36
|
+
Express tests in business language:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
describe('Shopping Cart', () => {
|
|
40
|
+
describe('given a cart with items', () => {
|
|
41
|
+
const cart = createCart([item1, item2]);
|
|
42
|
+
|
|
43
|
+
describe('when applying a discount code', () => {
|
|
44
|
+
const result = cart.applyDiscount('SAVE20');
|
|
45
|
+
|
|
46
|
+
it('then reduces the total by 20%', () => {
|
|
47
|
+
expect(result.total).toBe(originalTotal * 0.8);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('then marks discount as applied', () => {
|
|
51
|
+
expect(result.discountApplied).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Test Isolation
|
|
59
|
+
|
|
60
|
+
Each test must be completely independent:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// Bad: Shared mutable state
|
|
64
|
+
let counter = 0;
|
|
65
|
+
|
|
66
|
+
it('test 1', () => {
|
|
67
|
+
counter++;
|
|
68
|
+
expect(counter).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('test 2', () => {
|
|
72
|
+
expect(counter).toBe(0); // FAILS! Depends on test order
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Good: Fresh state per test
|
|
76
|
+
describe('Counter', () => {
|
|
77
|
+
let counter: Counter;
|
|
78
|
+
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
counter = new Counter();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('starts at zero', () => {
|
|
84
|
+
expect(counter.value).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('increments by one', () => {
|
|
88
|
+
counter.increment();
|
|
89
|
+
expect(counter.value).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Test Data Patterns
|
|
95
|
+
|
|
96
|
+
### Factory Pattern
|
|
97
|
+
|
|
98
|
+
Create test data with sensible defaults:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// factories/user.ts
|
|
102
|
+
import { faker } from '@faker-js/faker';
|
|
103
|
+
|
|
104
|
+
interface UserFactoryOptions {
|
|
105
|
+
email?: string;
|
|
106
|
+
role?: 'user' | 'admin';
|
|
107
|
+
verified?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const createTestUser = (overrides: UserFactoryOptions = {}): User => ({
|
|
111
|
+
id: faker.string.uuid(),
|
|
112
|
+
email: overrides.email ?? faker.internet.email(),
|
|
113
|
+
name: faker.person.fullName(),
|
|
114
|
+
role: overrides.role ?? 'user',
|
|
115
|
+
verified: overrides.verified ?? true,
|
|
116
|
+
createdAt: faker.date.past(),
|
|
117
|
+
updatedAt: new Date(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Usage - only specify what matters for the test
|
|
121
|
+
const admin = createTestUser({ role: 'admin' });
|
|
122
|
+
const unverified = createTestUser({ verified: false });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Builder Pattern
|
|
126
|
+
|
|
127
|
+
For complex objects with many configurations:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// builders/OrderBuilder.ts
|
|
131
|
+
export class OrderBuilder {
|
|
132
|
+
private order: Partial<Order> = {};
|
|
133
|
+
|
|
134
|
+
withId(id: string): this {
|
|
135
|
+
this.order.id = id;
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
withItems(items: OrderItem[]): this {
|
|
140
|
+
this.order.items = items;
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
withStatus(status: OrderStatus): this {
|
|
145
|
+
this.order.status = status;
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
withDiscount(discount: Discount): this {
|
|
150
|
+
this.order.discount = discount;
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
shipped(): this {
|
|
155
|
+
this.order.status = 'shipped';
|
|
156
|
+
this.order.shippedAt = new Date();
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
build(): Order {
|
|
161
|
+
return {
|
|
162
|
+
id: this.order.id ?? faker.string.uuid(),
|
|
163
|
+
items: this.order.items ?? [],
|
|
164
|
+
status: this.order.status ?? 'pending',
|
|
165
|
+
discount: this.order.discount,
|
|
166
|
+
createdAt: new Date(),
|
|
167
|
+
updatedAt: new Date(),
|
|
168
|
+
} as Order;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Usage
|
|
173
|
+
const order = new OrderBuilder()
|
|
174
|
+
.withItems([itemFactory()])
|
|
175
|
+
.withDiscount({ type: 'fixed', value: 10 })
|
|
176
|
+
.shipped()
|
|
177
|
+
.build();
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Object Mother Pattern
|
|
181
|
+
|
|
182
|
+
Pre-configured objects for common scenarios:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// test/mothers/userMother.ts
|
|
186
|
+
export const UserMother = {
|
|
187
|
+
admin: () => createTestUser({ role: 'admin', verified: true }),
|
|
188
|
+
unverified: () => createTestUser({ verified: false }),
|
|
189
|
+
suspended: () => createTestUser({ status: 'suspended' }),
|
|
190
|
+
withOrders: async () => {
|
|
191
|
+
const user = await db.user.create({ data: createTestUser() });
|
|
192
|
+
await db.order.createMany({
|
|
193
|
+
data: [
|
|
194
|
+
createTestOrder({ userId: user.id }),
|
|
195
|
+
createTestOrder({ userId: user.id }),
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
return user;
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Usage
|
|
203
|
+
const admin = UserMother.admin();
|
|
204
|
+
const userWithOrders = await UserMother.withOrders();
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Assertion Patterns
|
|
208
|
+
|
|
209
|
+
### Focused Assertions
|
|
210
|
+
|
|
211
|
+
One logical assertion per test:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
// Bad: Multiple unrelated assertions
|
|
215
|
+
it('processes order', async () => {
|
|
216
|
+
const order = await processOrder(orderData);
|
|
217
|
+
expect(order.id).toBeDefined();
|
|
218
|
+
expect(order.status).toBe('confirmed');
|
|
219
|
+
expect(order.total).toBe(100);
|
|
220
|
+
expect(order.items).toHaveLength(2);
|
|
221
|
+
expect(sendEmail).toHaveBeenCalled();
|
|
222
|
+
expect(decrementInventory).toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Good: Separate tests for each behavior
|
|
226
|
+
describe('processOrder', () => {
|
|
227
|
+
it('creates order with confirmed status', async () => {
|
|
228
|
+
const order = await processOrder(orderData);
|
|
229
|
+
expect(order.status).toBe('confirmed');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('calculates correct total', async () => {
|
|
233
|
+
const order = await processOrder(orderData);
|
|
234
|
+
expect(order.total).toBe(100);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('sends confirmation email', async () => {
|
|
238
|
+
await processOrder(orderData);
|
|
239
|
+
expect(sendEmail).toHaveBeenCalledWith(
|
|
240
|
+
expect.objectContaining({ type: 'order-confirmation' })
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Custom Matchers
|
|
247
|
+
|
|
248
|
+
Create domain-specific assertions:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
// test/matchers.ts
|
|
252
|
+
import { expect } from 'vitest';
|
|
253
|
+
|
|
254
|
+
expect.extend({
|
|
255
|
+
toBeValidEmail(received: string) {
|
|
256
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
257
|
+
const pass = emailRegex.test(received);
|
|
258
|
+
return {
|
|
259
|
+
pass,
|
|
260
|
+
message: () => `Expected ${received} ${pass ? 'not ' : ''}to be a valid email`,
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
toBeWithinRange(received: number, floor: number, ceiling: number) {
|
|
265
|
+
const pass = received >= floor && received <= ceiling;
|
|
266
|
+
return {
|
|
267
|
+
pass,
|
|
268
|
+
message: () =>
|
|
269
|
+
`Expected ${received} ${pass ? 'not ' : ''}to be within [${floor}, ${ceiling}]`,
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
toHaveBeenCalledWithUser(
|
|
274
|
+
received: jest.Mock,
|
|
275
|
+
expectedUser: Partial<User>
|
|
276
|
+
) {
|
|
277
|
+
const calls = received.mock.calls;
|
|
278
|
+
const pass = calls.some(([user]) =>
|
|
279
|
+
Object.entries(expectedUser).every(([key, value]) => user[key] === value)
|
|
280
|
+
);
|
|
281
|
+
return {
|
|
282
|
+
pass,
|
|
283
|
+
message: () =>
|
|
284
|
+
`Expected mock ${pass ? 'not ' : ''}to have been called with user matching ${JSON.stringify(expectedUser)}`,
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Usage
|
|
290
|
+
expect(user.email).toBeValidEmail();
|
|
291
|
+
expect(score).toBeWithinRange(0, 100);
|
|
292
|
+
expect(mockSendEmail).toHaveBeenCalledWithUser({ role: 'admin' });
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Snapshot Testing
|
|
296
|
+
|
|
297
|
+
Use sparingly for complex, stable outputs:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
// Good use: Serialized output that rarely changes
|
|
301
|
+
it('generates correct API response format', () => {
|
|
302
|
+
const response = formatApiResponse(data);
|
|
303
|
+
expect(response).toMatchSnapshot();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Bad use: Dynamic content that changes frequently
|
|
307
|
+
it('renders user profile', () => {
|
|
308
|
+
const profile = render(<UserProfile user={user} />);
|
|
309
|
+
expect(profile).toMatchSnapshot(); // Will break on any change
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Better: Inline snapshots for small outputs
|
|
313
|
+
it('formats currency correctly', () => {
|
|
314
|
+
expect(formatCurrency(1234.56, 'USD')).toMatchInlineSnapshot(`"$1,234.56"`);
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Test Organization Patterns
|
|
319
|
+
|
|
320
|
+
### Describe Blocks by Behavior
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
describe('OrderService', () => {
|
|
324
|
+
describe('createOrder', () => {
|
|
325
|
+
describe('with valid input', () => {
|
|
326
|
+
it('creates order record');
|
|
327
|
+
it('decrements inventory');
|
|
328
|
+
it('sends confirmation email');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('with invalid input', () => {
|
|
332
|
+
it('rejects empty cart');
|
|
333
|
+
it('rejects out-of-stock items');
|
|
334
|
+
it('rejects invalid user');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('cancelOrder', () => {
|
|
339
|
+
describe('when order is pending', () => {
|
|
340
|
+
it('updates status to cancelled');
|
|
341
|
+
it('restores inventory');
|
|
342
|
+
it('refunds payment');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('when order is shipped', () => {
|
|
346
|
+
it('throws CannotCancelError');
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Setup Hierarchy
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
describe('API Routes', () => {
|
|
356
|
+
// Runs once before all tests in this describe
|
|
357
|
+
beforeAll(async () => {
|
|
358
|
+
await db.$connect();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
afterAll(async () => {
|
|
362
|
+
await db.$disconnect();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('/users', () => {
|
|
366
|
+
// Runs before each test in /users
|
|
367
|
+
beforeEach(async () => {
|
|
368
|
+
await db.user.deleteMany();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('GET /users', () => {
|
|
372
|
+
it('returns empty list when no users');
|
|
373
|
+
it('returns all users');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe('POST /users', () => {
|
|
377
|
+
it('creates user with valid data');
|
|
378
|
+
it('rejects invalid email');
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('/orders', () => {
|
|
383
|
+
// Different setup for orders tests
|
|
384
|
+
beforeEach(async () => {
|
|
385
|
+
await db.order.deleteMany();
|
|
386
|
+
await db.user.deleteMany();
|
|
387
|
+
// Orders tests need a user
|
|
388
|
+
await db.user.create({ data: testUser });
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Anti-Patterns to Avoid
|
|
395
|
+
|
|
396
|
+
### Testing Implementation Details
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
// Bad: Couples test to implementation
|
|
400
|
+
it('uses lodash sortBy internally', () => {
|
|
401
|
+
const spy = vi.spyOn(_, 'sortBy');
|
|
402
|
+
sortUsers(users);
|
|
403
|
+
expect(spy).toHaveBeenCalled();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Good: Tests behavior
|
|
407
|
+
it('sorts users by name ascending', () => {
|
|
408
|
+
const result = sortUsers([
|
|
409
|
+
{ name: 'Charlie' },
|
|
410
|
+
{ name: 'Alice' },
|
|
411
|
+
{ name: 'Bob' },
|
|
412
|
+
]);
|
|
413
|
+
expect(result.map(u => u.name)).toEqual(['Alice', 'Bob', 'Charlie']);
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Excessive Mocking
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
// Bad: Everything is mocked
|
|
421
|
+
const mockDb = vi.fn();
|
|
422
|
+
const mockCache = vi.fn();
|
|
423
|
+
const mockLogger = vi.fn();
|
|
424
|
+
const mockConfig = vi.fn();
|
|
425
|
+
|
|
426
|
+
it('does something', () => {
|
|
427
|
+
// What are we even testing?
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Good: Integration test with real dependencies
|
|
431
|
+
it('persists user to database', async () => {
|
|
432
|
+
const user = await userService.create(userData);
|
|
433
|
+
const saved = await db.user.findUnique({ where: { id: user.id } });
|
|
434
|
+
expect(saved.email).toBe(userData.email);
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Test Interdependence
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
// Bad: Tests depend on each other
|
|
442
|
+
let createdId: string;
|
|
443
|
+
|
|
444
|
+
it('creates item', async () => {
|
|
445
|
+
const item = await create({ name: 'Test' });
|
|
446
|
+
createdId = item.id; // Stored for next test
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('gets item', async () => {
|
|
450
|
+
const item = await get(createdId); // Depends on previous test
|
|
451
|
+
expect(item.name).toBe('Test');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Good: Each test is self-contained
|
|
455
|
+
it('creates and retrieves item', async () => {
|
|
456
|
+
const created = await create({ name: 'Test' });
|
|
457
|
+
const retrieved = await get(created.id);
|
|
458
|
+
expect(retrieved.name).toBe('Test');
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Magic Values
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
// Bad: What does 42 mean?
|
|
466
|
+
it('calculates something', () => {
|
|
467
|
+
expect(calculate(42)).toBe(84);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Good: Named constants explain intent
|
|
471
|
+
it('doubles the input value', () => {
|
|
472
|
+
const input = 42;
|
|
473
|
+
const expected = input * 2;
|
|
474
|
+
expect(double(input)).toBe(expected);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Or with descriptive test name
|
|
478
|
+
it('returns double the input', () => {
|
|
479
|
+
expect(double(5)).toBe(10);
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
## Naming Conventions
|
|
484
|
+
|
|
485
|
+
### Test Names Should Be Sentences
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
// Bad: Vague names
|
|
489
|
+
it('works');
|
|
490
|
+
it('handles error');
|
|
491
|
+
it('test 1');
|
|
492
|
+
|
|
493
|
+
// Good: Descriptive sentences
|
|
494
|
+
it('returns empty array when no users exist');
|
|
495
|
+
it('throws NotFoundError when user does not exist');
|
|
496
|
+
it('sends welcome email after user registration');
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Use Consistent Prefixes
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
// Action verbs
|
|
503
|
+
it('creates order with valid data');
|
|
504
|
+
it('updates user profile');
|
|
505
|
+
it('deletes expired sessions');
|
|
506
|
+
|
|
507
|
+
// Condition verbs
|
|
508
|
+
it('returns null when user not found');
|
|
509
|
+
it('throws ValidationError for invalid email');
|
|
510
|
+
it('rejects duplicate email addresses');
|
|
511
|
+
```
|