agentic-team-templates 0.4.2 → 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.
@@ -0,0 +1,163 @@
1
+ # Testing Best Practices
2
+
3
+ Comprehensive guidelines for building world-class, principal-level test suites.
4
+
5
+ ## Scope
6
+
7
+ This ruleset applies to:
8
+ - Unit testing
9
+ - Integration testing
10
+ - End-to-end (E2E) testing
11
+ - Performance and load testing
12
+ - Contract testing
13
+ - Property-based testing
14
+ - Mutation testing
15
+ - Chaos engineering tests
16
+
17
+ ## Core Philosophy
18
+
19
+ **Tests are a first-class deliverable.** A feature without tests is incomplete. Tests provide confidence to ship, enable rapid iteration, and serve as executable documentation.
20
+
21
+ ## Testing Trophy (Not Pyramid)
22
+
23
+ The Testing Trophy prioritizes integration tests over unit tests for maximum real-world value:
24
+
25
+ ```
26
+ ┌─────────────┐
27
+ │ E2E │ ~10% - Critical user journeys only
28
+ ├─────────────┤
29
+ │ │
30
+ │ Integration │ ~60% - Maximum confidence/cost ratio
31
+ │ │
32
+ ├─────────────┤
33
+ │ Unit │ ~20% - Pure functions and logic
34
+ ├─────────────┤
35
+ │ Static │ ~10% - Types, linting, formatting
36
+ └─────────────┘
37
+ ```
38
+
39
+ ## Key Principles
40
+
41
+ ### 1. Test Behavior, Not Implementation
42
+
43
+ Tests should verify what the system does, not how it does it. This allows refactoring without breaking tests.
44
+
45
+ ```ts
46
+ // Bad: Tests implementation
47
+ it('calls repository.save', () => {
48
+ const spy = vi.spyOn(repo, 'save');
49
+ service.createUser(data);
50
+ expect(spy).toHaveBeenCalled();
51
+ });
52
+
53
+ // Good: Tests behavior
54
+ it('persists user to database', async () => {
55
+ await service.createUser(data);
56
+ const user = await db.user.findUnique({ where: { email: data.email } });
57
+ expect(user).toBeDefined();
58
+ });
59
+ ```
60
+
61
+ ### 2. Fast Feedback Loops
62
+
63
+ Tests must run quickly to be useful. Slow tests get skipped or ignored.
64
+
65
+ | Test Type | Target Time |
66
+ |-----------|-------------|
67
+ | Unit tests | < 10ms each |
68
+ | Integration tests | < 500ms each |
69
+ | E2E tests | < 30s each |
70
+ | Full suite | < 5 minutes |
71
+
72
+ ### 3. Deterministic Results
73
+
74
+ Same inputs must produce same outputs, always. No flaky tests allowed.
75
+
76
+ - Mock time-dependent code
77
+ - Isolate test data
78
+ - Reset state between tests
79
+ - Use explicit waits, not sleeps
80
+
81
+ ### 4. Tests as Documentation
82
+
83
+ Tests describe system behavior. Anyone should be able to understand what the code does by reading its tests.
84
+
85
+ ```ts
86
+ describe('OrderService', () => {
87
+ describe('when cart has items', () => {
88
+ it('creates order with correct total');
89
+ it('decrements inventory for each item');
90
+ it('sends confirmation email to user');
91
+ it('returns order confirmation number');
92
+ });
93
+
94
+ describe('when cart is empty', () => {
95
+ it('throws EmptyCartError');
96
+ it('does not create order record');
97
+ });
98
+ });
99
+ ```
100
+
101
+ ### 5. Write Tests First (TDD)
102
+
103
+ For complex logic, write the test before the implementation:
104
+
105
+ 1. **Red** - Write a failing test
106
+ 2. **Green** - Write minimal code to pass
107
+ 3. **Refactor** - Improve code quality
108
+
109
+ ## Technology Stack
110
+
111
+ ### Recommended Tools
112
+
113
+ | Purpose | Tool | Why |
114
+ |---------|------|-----|
115
+ | Unit/Integration | Vitest | Fast, modern, ESM native |
116
+ | E2E | Playwright | Cross-browser, reliable |
117
+ | Performance | k6 | Developer-friendly, scriptable |
118
+ | Contract | Pact | Consumer-driven, battle-tested |
119
+ | Property | fast-check | Finds edge cases automatically |
120
+ | Mutation | Stryker | Validates test quality |
121
+ | Mocking | MSW | Network-level mocking |
122
+
123
+ ### Vitest Configuration
124
+
125
+ ```ts
126
+ // vitest.config.ts
127
+ import { defineConfig } from 'vitest/config';
128
+
129
+ export default defineConfig({
130
+ test: {
131
+ globals: true,
132
+ environment: 'node',
133
+ include: ['**/*.test.ts', '**/*.spec.ts'],
134
+ coverage: {
135
+ provider: 'v8',
136
+ reporter: ['text', 'json', 'html'],
137
+ exclude: ['**/node_modules/**', '**/test/**'],
138
+ thresholds: {
139
+ lines: 80,
140
+ branches: 75,
141
+ functions: 90,
142
+ },
143
+ },
144
+ sequence: {
145
+ shuffle: true, // Catch order dependencies
146
+ },
147
+ },
148
+ });
149
+ ```
150
+
151
+ ## Definition of Done
152
+
153
+ A test suite is complete when:
154
+
155
+ - [ ] Critical paths have integration tests
156
+ - [ ] Pure functions have unit tests
157
+ - [ ] Edge cases are explicitly tested
158
+ - [ ] Error paths are tested
159
+ - [ ] Tests are deterministic
160
+ - [ ] No flaky tests
161
+ - [ ] Coverage meets thresholds
162
+ - [ ] Tests run in under 5 minutes
163
+ - [ ] Documentation is current
@@ -0,0 +1,536 @@
1
+ # Performance Testing
2
+
3
+ Guidelines for load testing, stress testing, and performance validation.
4
+
5
+ ## Performance Test Types
6
+
7
+ | Type | Purpose | Duration | Load Pattern |
8
+ |------|---------|----------|--------------|
9
+ | Smoke | Verify system works | 1-2 min | Minimal load |
10
+ | Load | Normal traffic | 10-30 min | Expected users |
11
+ | Stress | Beyond capacity | 30-60 min | Increasing load |
12
+ | Spike | Sudden surge | 10-20 min | Sharp increase |
13
+ | Soak | Extended period | 4-24 hours | Steady load |
14
+ | Breakpoint | Find limits | Until failure | Increasing until break |
15
+
16
+ ## k6 Performance Testing
17
+
18
+ k6 is a modern, developer-friendly load testing tool.
19
+
20
+ ### Basic Load Test
21
+
22
+ ```js
23
+ // load-tests/smoke.js
24
+ import http from 'k6/http';
25
+ import { check, sleep } from 'k6';
26
+
27
+ export const options = {
28
+ vus: 1,
29
+ duration: '1m',
30
+ thresholds: {
31
+ http_req_duration: ['p(95)<500'],
32
+ http_req_failed: ['rate<0.01'],
33
+ },
34
+ };
35
+
36
+ export default function () {
37
+ const response = http.get('http://api.example.com/health');
38
+
39
+ check(response, {
40
+ 'status is 200': (r) => r.status === 200,
41
+ 'response time < 500ms': (r) => r.timings.duration < 500,
42
+ });
43
+
44
+ sleep(1);
45
+ }
46
+ ```
47
+
48
+ ### Staged Load Test
49
+
50
+ ```js
51
+ // load-tests/load.js
52
+ import http from 'k6/http';
53
+ import { check, sleep } from 'k6';
54
+
55
+ export const options = {
56
+ stages: [
57
+ { duration: '2m', target: 50 }, // Ramp up to 50 users
58
+ { duration: '5m', target: 50 }, // Stay at 50 users
59
+ { duration: '2m', target: 100 }, // Ramp up to 100 users
60
+ { duration: '5m', target: 100 }, // Stay at 100 users
61
+ { duration: '2m', target: 0 }, // Ramp down
62
+ ],
63
+ thresholds: {
64
+ http_req_duration: ['p(95)<500', 'p(99)<1000'],
65
+ http_req_failed: ['rate<0.01'],
66
+ checks: ['rate>0.99'],
67
+ },
68
+ };
69
+
70
+ export default function () {
71
+ const baseUrl = 'http://api.example.com';
72
+
73
+ // Simulate user journey
74
+ const productsRes = http.get(`${baseUrl}/products`);
75
+ check(productsRes, { 'products 200': (r) => r.status === 200 });
76
+
77
+ const products = JSON.parse(productsRes.body);
78
+ if (products.length > 0) {
79
+ const productId = products[0].id;
80
+ const productRes = http.get(`${baseUrl}/products/${productId}`);
81
+ check(productRes, { 'product 200': (r) => r.status === 200 });
82
+ }
83
+
84
+ sleep(Math.random() * 3 + 1); // Random think time 1-4s
85
+ }
86
+ ```
87
+
88
+ ### Stress Test
89
+
90
+ ```js
91
+ // load-tests/stress.js
92
+ import http from 'k6/http';
93
+ import { check, sleep } from 'k6';
94
+
95
+ export const options = {
96
+ stages: [
97
+ { duration: '2m', target: 100 }, // Below normal
98
+ { duration: '5m', target: 100 },
99
+ { duration: '2m', target: 200 }, // Normal load
100
+ { duration: '5m', target: 200 },
101
+ { duration: '2m', target: 300 }, // Around breaking point
102
+ { duration: '5m', target: 300 },
103
+ { duration: '2m', target: 400 }, // Beyond breaking point
104
+ { duration: '5m', target: 400 },
105
+ { duration: '10m', target: 0 }, // Recovery
106
+ ],
107
+ thresholds: {
108
+ http_req_duration: ['p(99)<1500'],
109
+ http_req_failed: ['rate<0.05'], // Allow 5% failure under stress
110
+ },
111
+ };
112
+
113
+ export default function () {
114
+ const response = http.post(
115
+ 'http://api.example.com/orders',
116
+ JSON.stringify({
117
+ items: [{ productId: '1', quantity: 1 }],
118
+ }),
119
+ {
120
+ headers: { 'Content-Type': 'application/json' },
121
+ }
122
+ );
123
+
124
+ check(response, {
125
+ 'order created': (r) => r.status === 201 || r.status === 429, // Accept rate limiting
126
+ });
127
+
128
+ sleep(1);
129
+ }
130
+ ```
131
+
132
+ ### Spike Test
133
+
134
+ ```js
135
+ // load-tests/spike.js
136
+ import http from 'k6/http';
137
+ import { check, sleep } from 'k6';
138
+
139
+ export const options = {
140
+ stages: [
141
+ { duration: '1m', target: 50 }, // Warm up
142
+ { duration: '10s', target: 500 }, // Spike!
143
+ { duration: '3m', target: 500 }, // Stay at spike
144
+ { duration: '10s', target: 50 }, // Scale down
145
+ { duration: '3m', target: 50 }, // Recovery
146
+ { duration: '1m', target: 0 }, // Ramp down
147
+ ],
148
+ thresholds: {
149
+ http_req_duration: ['p(99)<2000'],
150
+ http_req_failed: ['rate<0.10'], // Allow 10% during spike
151
+ },
152
+ };
153
+
154
+ export default function () {
155
+ const response = http.get('http://api.example.com/products');
156
+ check(response, { 'status 200': (r) => r.status === 200 });
157
+ sleep(0.5);
158
+ }
159
+ ```
160
+
161
+ ## Real User Scenarios
162
+
163
+ ### Authenticated User Flow
164
+
165
+ ```js
166
+ // load-tests/authenticated-flow.js
167
+ import http from 'k6/http';
168
+ import { check, group, sleep } from 'k6';
169
+
170
+ const BASE_URL = 'http://api.example.com';
171
+
172
+ export const options = {
173
+ vus: 50,
174
+ duration: '10m',
175
+ thresholds: {
176
+ 'http_req_duration{name:login}': ['p(95)<1000'],
177
+ 'http_req_duration{name:dashboard}': ['p(95)<500'],
178
+ 'http_req_duration{name:order}': ['p(95)<2000'],
179
+ },
180
+ };
181
+
182
+ export default function () {
183
+ let authToken;
184
+
185
+ group('Login', () => {
186
+ const loginRes = http.post(
187
+ `${BASE_URL}/auth/login`,
188
+ JSON.stringify({
189
+ email: `user${__VU}@test.com`,
190
+ password: 'testpassword',
191
+ }),
192
+ {
193
+ headers: { 'Content-Type': 'application/json' },
194
+ tags: { name: 'login' },
195
+ }
196
+ );
197
+
198
+ check(loginRes, { 'login successful': (r) => r.status === 200 });
199
+ authToken = JSON.parse(loginRes.body).token;
200
+ });
201
+
202
+ const authHeaders = {
203
+ 'Content-Type': 'application/json',
204
+ Authorization: `Bearer ${authToken}`,
205
+ };
206
+
207
+ sleep(2);
208
+
209
+ group('Browse Dashboard', () => {
210
+ const dashRes = http.get(`${BASE_URL}/dashboard`, {
211
+ headers: authHeaders,
212
+ tags: { name: 'dashboard' },
213
+ });
214
+ check(dashRes, { 'dashboard loaded': (r) => r.status === 200 });
215
+ });
216
+
217
+ sleep(3);
218
+
219
+ group('Create Order', () => {
220
+ const orderRes = http.post(
221
+ `${BASE_URL}/orders`,
222
+ JSON.stringify({
223
+ items: [
224
+ { productId: '1', quantity: 2 },
225
+ { productId: '2', quantity: 1 },
226
+ ],
227
+ }),
228
+ {
229
+ headers: authHeaders,
230
+ tags: { name: 'order' },
231
+ }
232
+ );
233
+ check(orderRes, { 'order created': (r) => r.status === 201 });
234
+ });
235
+
236
+ sleep(1);
237
+ }
238
+ ```
239
+
240
+ ### Mixed Workload
241
+
242
+ ```js
243
+ // load-tests/mixed-workload.js
244
+ import http from 'k6/http';
245
+ import { check, sleep } from 'k6';
246
+ import { randomItem } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
247
+
248
+ const scenarios = {
249
+ browsers: {
250
+ executor: 'constant-vus',
251
+ vus: 30,
252
+ duration: '10m',
253
+ exec: 'browseProducts',
254
+ },
255
+ buyers: {
256
+ executor: 'ramping-vus',
257
+ startVUs: 0,
258
+ stages: [
259
+ { duration: '2m', target: 10 },
260
+ { duration: '6m', target: 10 },
261
+ { duration: '2m', target: 0 },
262
+ ],
263
+ exec: 'purchaseProduct',
264
+ },
265
+ api_users: {
266
+ executor: 'constant-arrival-rate',
267
+ rate: 100,
268
+ timeUnit: '1s',
269
+ duration: '10m',
270
+ preAllocatedVUs: 50,
271
+ exec: 'apiCalls',
272
+ },
273
+ };
274
+
275
+ export const options = { scenarios };
276
+
277
+ export function browseProducts() {
278
+ const res = http.get('http://api.example.com/products');
279
+ check(res, { 'browse ok': (r) => r.status === 200 });
280
+ sleep(Math.random() * 5 + 2);
281
+ }
282
+
283
+ export function purchaseProduct() {
284
+ // Login
285
+ const login = http.post('http://api.example.com/auth/login', {
286
+ email: `buyer${__VU}@test.com`,
287
+ password: 'test',
288
+ });
289
+
290
+ if (login.status !== 200) return;
291
+
292
+ const token = JSON.parse(login.body).token;
293
+ const headers = { Authorization: `Bearer ${token}` };
294
+
295
+ // Purchase
296
+ const order = http.post(
297
+ 'http://api.example.com/orders',
298
+ JSON.stringify({ items: [{ productId: '1', quantity: 1 }] }),
299
+ { headers }
300
+ );
301
+
302
+ check(order, { 'purchase ok': (r) => r.status === 201 });
303
+ sleep(1);
304
+ }
305
+
306
+ export function apiCalls() {
307
+ const endpoints = ['/products', '/categories', '/featured'];
308
+ const endpoint = randomItem(endpoints);
309
+ const res = http.get(`http://api.example.com${endpoint}`);
310
+ check(res, { 'api ok': (r) => r.status === 200 });
311
+ }
312
+ ```
313
+
314
+ ## Performance Thresholds
315
+
316
+ ### Defining SLOs
317
+
318
+ ```js
319
+ export const options = {
320
+ thresholds: {
321
+ // Response time
322
+ http_req_duration: [
323
+ 'p(50)<200', // 50% under 200ms
324
+ 'p(90)<500', // 90% under 500ms
325
+ 'p(95)<800', // 95% under 800ms
326
+ 'p(99)<1500', // 99% under 1500ms
327
+ ],
328
+
329
+ // Error rate
330
+ http_req_failed: ['rate<0.01'], // Less than 1%
331
+
332
+ // Throughput
333
+ http_reqs: ['rate>100'], // At least 100 RPS
334
+
335
+ // Custom metrics
336
+ 'http_req_duration{name:checkout}': ['p(95)<2000'],
337
+ 'http_req_duration{name:search}': ['p(95)<300'],
338
+
339
+ // Check pass rate
340
+ checks: ['rate>0.99'],
341
+ },
342
+ };
343
+ ```
344
+
345
+ ### Custom Metrics
346
+
347
+ ```js
348
+ import { Trend, Rate, Counter, Gauge } from 'k6/metrics';
349
+
350
+ const orderDuration = new Trend('order_duration');
351
+ const orderSuccess = new Rate('order_success');
352
+ const ordersCreated = new Counter('orders_created');
353
+ const activeUsers = new Gauge('active_users');
354
+
355
+ export default function () {
356
+ activeUsers.add(__VU);
357
+
358
+ const start = Date.now();
359
+ const response = http.post('http://api.example.com/orders', orderData);
360
+ const duration = Date.now() - start;
361
+
362
+ orderDuration.add(duration);
363
+ orderSuccess.add(response.status === 201);
364
+
365
+ if (response.status === 201) {
366
+ ordersCreated.add(1);
367
+ }
368
+ }
369
+ ```
370
+
371
+ ## CI Integration
372
+
373
+ ### Performance Regression Testing
374
+
375
+ ```yaml
376
+ # .github/workflows/performance.yml
377
+ name: Performance Tests
378
+
379
+ on:
380
+ push:
381
+ branches: [main]
382
+ schedule:
383
+ - cron: '0 2 * * *' # Nightly
384
+
385
+ jobs:
386
+ smoke-test:
387
+ runs-on: ubuntu-latest
388
+ steps:
389
+ - uses: actions/checkout@v4
390
+
391
+ - name: Start Application
392
+ run: docker-compose up -d
393
+
394
+ - name: Wait for healthy
395
+ run: |
396
+ for i in {1..30}; do
397
+ if curl -s http://localhost:3000/health | grep -q "ok"; then
398
+ exit 0
399
+ fi
400
+ sleep 2
401
+ done
402
+ exit 1
403
+
404
+ - name: Run Smoke Test
405
+ uses: grafana/k6-action@v0.3.1
406
+ with:
407
+ filename: load-tests/smoke.js
408
+ flags: --out json=results.json
409
+
410
+ - name: Check Results
411
+ run: |
412
+ if jq -e '.metrics.http_req_failed.values.rate > 0.01' results.json; then
413
+ echo "Error rate too high"
414
+ exit 1
415
+ fi
416
+
417
+ load-test:
418
+ runs-on: ubuntu-latest
419
+ if: github.ref == 'refs/heads/main'
420
+ needs: smoke-test
421
+ steps:
422
+ - uses: actions/checkout@v4
423
+
424
+ - name: Deploy to staging
425
+ run: ./deploy-staging.sh
426
+
427
+ - name: Run Load Test
428
+ uses: grafana/k6-action@v0.3.1
429
+ with:
430
+ filename: load-tests/load.js
431
+ cloud: true
432
+ env:
433
+ K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
434
+
435
+ - name: Upload Results
436
+ uses: actions/upload-artifact@v4
437
+ with:
438
+ name: k6-results
439
+ path: results/
440
+ ```
441
+
442
+ ### Baseline Comparison
443
+
444
+ ```js
445
+ // load-tests/baseline.js
446
+ import http from 'k6/http';
447
+ import { check } from 'k6';
448
+
449
+ const BASELINE = {
450
+ p95_latency: 500,
451
+ error_rate: 0.01,
452
+ rps: 100,
453
+ };
454
+
455
+ export const options = {
456
+ vus: 50,
457
+ duration: '5m',
458
+ thresholds: {
459
+ http_req_duration: [`p(95)<${BASELINE.p95_latency * 1.1}`], // 10% tolerance
460
+ http_req_failed: [`rate<${BASELINE.error_rate * 1.5}`],
461
+ http_reqs: [`rate>${BASELINE.rps * 0.9}`],
462
+ },
463
+ };
464
+
465
+ export default function () {
466
+ const res = http.get('http://api.example.com/products');
467
+ check(res, { 'ok': (r) => r.status === 200 });
468
+ }
469
+
470
+ export function handleSummary(data) {
471
+ const current = {
472
+ p95_latency: data.metrics.http_req_duration.values['p(95)'],
473
+ error_rate: data.metrics.http_req_failed.values.rate,
474
+ rps: data.metrics.http_reqs.values.rate,
475
+ };
476
+
477
+ console.log('Baseline Comparison:');
478
+ console.log(` p95 Latency: ${current.p95_latency}ms (baseline: ${BASELINE.p95_latency}ms)`);
479
+ console.log(` Error Rate: ${(current.error_rate * 100).toFixed(2)}% (baseline: ${BASELINE.error_rate * 100}%)`);
480
+ console.log(` RPS: ${current.rps.toFixed(0)} (baseline: ${BASELINE.rps})`);
481
+
482
+ return {
483
+ 'results.json': JSON.stringify({ baseline: BASELINE, current }),
484
+ };
485
+ }
486
+ ```
487
+
488
+ ## Best Practices
489
+
490
+ ### 1. Test in Production-like Environment
491
+
492
+ - Same infrastructure
493
+ - Similar data volume
494
+ - Realistic network conditions
495
+
496
+ ### 2. Use Realistic Data
497
+
498
+ ```js
499
+ // Use varied test data
500
+ const users = JSON.parse(open('./testdata/users.json'));
501
+ const products = JSON.parse(open('./testdata/products.json'));
502
+
503
+ export default function () {
504
+ const user = users[__VU % users.length];
505
+ const product = products[Math.floor(Math.random() * products.length)];
506
+ // ...
507
+ }
508
+ ```
509
+
510
+ ### 3. Include Think Time
511
+
512
+ ```js
513
+ // Realistic user behavior includes pauses
514
+ sleep(Math.random() * 3 + 1); // 1-4 seconds
515
+ ```
516
+
517
+ ### 4. Warm Up Before Testing
518
+
519
+ ```js
520
+ export const options = {
521
+ stages: [
522
+ { duration: '2m', target: 10 }, // Warm up
523
+ { duration: '5m', target: 100 }, // Actual test
524
+ // ...
525
+ ],
526
+ };
527
+ ```
528
+
529
+ ### 5. Monitor Server Metrics
530
+
531
+ Track alongside load tests:
532
+ - CPU usage
533
+ - Memory usage
534
+ - Database connections
535
+ - Queue depths
536
+ - Error logs