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,596 @@
1
+ # Advanced Testing Techniques
2
+
3
+ Guidelines for property-based testing, mutation testing, contract testing, and chaos engineering.
4
+
5
+ ## Property-Based Testing
6
+
7
+ Instead of testing specific examples, test properties that should hold for ALL inputs.
8
+
9
+ ### Core Concept
10
+
11
+ ```ts
12
+ // Example-based: Tests specific cases
13
+ it('sorts numbers ascending', () => {
14
+ expect(sort([3, 1, 2])).toEqual([1, 2, 3]);
15
+ expect(sort([5, 4])).toEqual([4, 5]);
16
+ });
17
+
18
+ // Property-based: Tests invariants
19
+ it('sorted array maintains length', () => {
20
+ fc.assert(
21
+ fc.property(fc.array(fc.integer()), (arr) => {
22
+ return sort(arr).length === arr.length;
23
+ })
24
+ );
25
+ });
26
+ ```
27
+
28
+ ### Using fast-check
29
+
30
+ ```ts
31
+ import { fc } from '@fast-check/vitest';
32
+ import { describe, it, expect } from 'vitest';
33
+
34
+ describe('Array operations', () => {
35
+ it.prop([fc.array(fc.integer())])('sort is idempotent', (arr) => {
36
+ expect(sort(sort(arr))).toEqual(sort(arr));
37
+ });
38
+
39
+ it.prop([fc.array(fc.integer())])('sort maintains elements', (arr) => {
40
+ const sorted = sort(arr);
41
+ const original = [...arr].sort((a, b) => a - b);
42
+ expect(sorted).toEqual(original);
43
+ });
44
+
45
+ it.prop([fc.array(fc.integer())])('sorted array is ordered', (arr) => {
46
+ const sorted = sort(arr);
47
+ for (let i = 1; i < sorted.length; i++) {
48
+ expect(sorted[i]).toBeGreaterThanOrEqual(sorted[i - 1]);
49
+ }
50
+ });
51
+ });
52
+ ```
53
+
54
+ ### Common Properties to Test
55
+
56
+ ```ts
57
+ // Roundtrip: encode/decode returns original
58
+ it.prop([fc.string()])('JSON roundtrip', (str) => {
59
+ expect(JSON.parse(JSON.stringify(str))).toEqual(str);
60
+ });
61
+
62
+ // Symmetry: operation and inverse cancel out
63
+ it.prop([fc.integer()])('negate is symmetric', (n) => {
64
+ expect(negate(negate(n))).toEqual(n);
65
+ });
66
+
67
+ // Invariant: property always holds
68
+ it.prop([fc.array(fc.integer())])('length is non-negative', (arr) => {
69
+ expect(arr.length).toBeGreaterThanOrEqual(0);
70
+ });
71
+
72
+ // Idempotence: applying twice = applying once
73
+ it.prop([fc.string()])('trim is idempotent', (str) => {
74
+ expect(str.trim().trim()).toEqual(str.trim());
75
+ });
76
+
77
+ // Commutative: order doesn't matter
78
+ it.prop([fc.integer(), fc.integer()])('add is commutative', (a, b) => {
79
+ expect(add(a, b)).toEqual(add(b, a));
80
+ });
81
+
82
+ // Associative: grouping doesn't matter
83
+ it.prop([fc.integer(), fc.integer(), fc.integer()])('add is associative', (a, b, c) => {
84
+ expect(add(add(a, b), c)).toEqual(add(a, add(b, c)));
85
+ });
86
+ ```
87
+
88
+ ### Custom Arbitraries
89
+
90
+ ```ts
91
+ // Custom user generator
92
+ const userArbitrary = fc.record({
93
+ id: fc.uuid(),
94
+ email: fc.emailAddress(),
95
+ name: fc.string({ minLength: 1, maxLength: 100 }),
96
+ age: fc.integer({ min: 18, max: 120 }),
97
+ role: fc.constantFrom('user', 'admin', 'moderator'),
98
+ });
99
+
100
+ // Custom order generator
101
+ const orderArbitrary = fc.record({
102
+ id: fc.uuid(),
103
+ items: fc.array(
104
+ fc.record({
105
+ productId: fc.uuid(),
106
+ quantity: fc.integer({ min: 1, max: 100 }),
107
+ price: fc.float({ min: 0.01, max: 10000 }),
108
+ }),
109
+ { minLength: 1, maxLength: 10 }
110
+ ),
111
+ });
112
+
113
+ it.prop([userArbitrary])('user email is always valid', (user) => {
114
+ expect(isValidEmail(user.email)).toBe(true);
115
+ });
116
+
117
+ it.prop([orderArbitrary])('order total is positive', (order) => {
118
+ const total = calculateTotal(order);
119
+ expect(total).toBeGreaterThan(0);
120
+ });
121
+ ```
122
+
123
+ ### Shrinking
124
+
125
+ fast-check automatically finds minimal failing cases:
126
+
127
+ ```ts
128
+ // If this fails for [1000, -500, 42]
129
+ // fast-check will shrink to find minimal case like [1, -1]
130
+ it.prop([fc.array(fc.integer())])('buggy function', (arr) => {
131
+ expect(buggyFunction(arr)).toBe(true);
132
+ });
133
+ ```
134
+
135
+ ## Mutation Testing
136
+
137
+ Test your tests by introducing bugs and checking if tests catch them.
138
+
139
+ ### How It Works
140
+
141
+ 1. **Mutate**: Change code in small ways
142
+ 2. **Test**: Run test suite
143
+ 3. **Analyze**: Did tests catch the change?
144
+
145
+ ```ts
146
+ // Original
147
+ function isAdult(age: number): boolean {
148
+ return age >= 18;
149
+ }
150
+
151
+ // Mutations
152
+ return age > 18; // Boundary mutation
153
+ return age >= 17; // Constant mutation
154
+ return age <= 18; // Operator mutation
155
+ return true; // Return value mutation
156
+ ```
157
+
158
+ ### Stryker Configuration
159
+
160
+ ```json
161
+ // stryker.conf.json
162
+ {
163
+ "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
164
+ "packageManager": "npm",
165
+ "testRunner": "vitest",
166
+ "reporters": ["clear-text", "html", "dashboard"],
167
+ "mutate": [
168
+ "src/**/*.ts",
169
+ "!src/**/*.test.ts",
170
+ "!src/**/*.d.ts"
171
+ ],
172
+ "thresholds": {
173
+ "high": 80,
174
+ "low": 60,
175
+ "break": 50
176
+ },
177
+ "mutator": {
178
+ "excludedMutations": [
179
+ "StringLiteral",
180
+ "ObjectLiteral"
181
+ ]
182
+ },
183
+ "concurrency": 4,
184
+ "timeoutMS": 10000
185
+ }
186
+ ```
187
+
188
+ ### Interpreting Results
189
+
190
+ ```
191
+ Mutation testing finished.
192
+ Ran 847 tests against 124 mutants.
193
+
194
+ ┌─────────────────────────────────────────────────────┐
195
+ │ File │ % Score │ Killed │ Survived │
196
+ ├─────────────────────────────────────────────────────┤
197
+ │ src/services/order.ts │ 78.3% │ 47 │ 13 │
198
+ │ src/services/user.ts │ 91.2% │ 31 │ 3 │
199
+ │ src/utils/validation.ts │ 95.0% │ 19 │ 1 │
200
+ └─────────────────────────────────────────────────────┘
201
+
202
+ Mutation score: 85.48%
203
+ ```
204
+
205
+ ### Fixing Survived Mutants
206
+
207
+ ```ts
208
+ // Survived mutant: age >= 18 → age > 18
209
+ // This means no test checks the boundary condition
210
+
211
+ // Add test for boundary
212
+ it('person aged 18 is adult', () => {
213
+ expect(isAdult(18)).toBe(true);
214
+ });
215
+
216
+ it('person aged 17 is not adult', () => {
217
+ expect(isAdult(17)).toBe(false);
218
+ });
219
+ ```
220
+
221
+ ## Contract Testing
222
+
223
+ Verify that services can communicate correctly without running them together.
224
+
225
+ ### Consumer-Driven Contracts with Pact
226
+
227
+ ```ts
228
+ // Consumer side: Define expectations
229
+ import { PactV3, MatchersV3 } from '@pact-foundation/pact';
230
+
231
+ const { like, eachLike, regex } = MatchersV3;
232
+
233
+ const provider = new PactV3({
234
+ consumer: 'OrderService',
235
+ provider: 'UserService',
236
+ logLevel: 'warn',
237
+ });
238
+
239
+ describe('User API Contract', () => {
240
+ it('gets user by ID', async () => {
241
+ await provider
242
+ .given('user 123 exists')
243
+ .uponReceiving('a request for user 123')
244
+ .withRequest({
245
+ method: 'GET',
246
+ path: '/users/123',
247
+ headers: {
248
+ Accept: 'application/json',
249
+ },
250
+ })
251
+ .willRespondWith({
252
+ status: 200,
253
+ headers: {
254
+ 'Content-Type': 'application/json',
255
+ },
256
+ body: {
257
+ id: like('123'),
258
+ name: like('John Doe'),
259
+ email: regex(/^[\w-]+@[\w-]+\.\w+$/, 'john@example.com'),
260
+ role: like('user'),
261
+ },
262
+ });
263
+
264
+ await provider.executeTest(async (mockServer) => {
265
+ const client = new UserApiClient(mockServer.url);
266
+ const user = await client.getUser('123');
267
+
268
+ expect(user.id).toBe('123');
269
+ expect(user.email).toContain('@');
270
+ });
271
+ });
272
+
273
+ it('handles user not found', async () => {
274
+ await provider
275
+ .given('user does not exist')
276
+ .uponReceiving('a request for nonexistent user')
277
+ .withRequest({
278
+ method: 'GET',
279
+ path: '/users/nonexistent',
280
+ })
281
+ .willRespondWith({
282
+ status: 404,
283
+ body: {
284
+ error: like('User not found'),
285
+ },
286
+ });
287
+
288
+ await provider.executeTest(async (mockServer) => {
289
+ const client = new UserApiClient(mockServer.url);
290
+ await expect(client.getUser('nonexistent')).rejects.toThrow('User not found');
291
+ });
292
+ });
293
+ });
294
+ ```
295
+
296
+ ### Provider Verification
297
+
298
+ ```ts
299
+ // Provider side: Verify against contracts
300
+ import { Verifier } from '@pact-foundation/pact';
301
+
302
+ describe('User Service Provider', () => {
303
+ it('validates the contract with OrderService', async () => {
304
+ const verifier = new Verifier({
305
+ providerBaseUrl: 'http://localhost:3000',
306
+ pactUrls: ['./pacts/orderservice-userservice.json'],
307
+ // Or from Pact Broker
308
+ // pactBrokerUrl: 'https://pact-broker.example.com',
309
+ // providerVersion: process.env.GIT_SHA,
310
+ stateHandlers: {
311
+ 'user 123 exists': async () => {
312
+ await db.user.create({
313
+ data: { id: '123', name: 'John Doe', email: 'john@example.com' },
314
+ });
315
+ },
316
+ 'user does not exist': async () => {
317
+ await db.user.deleteMany();
318
+ },
319
+ },
320
+ });
321
+
322
+ await verifier.verifyProvider();
323
+ });
324
+ });
325
+ ```
326
+
327
+ ### Publishing Contracts
328
+
329
+ ```yaml
330
+ # CI workflow
331
+ - name: Publish Pact
332
+ run: |
333
+ npx pact-broker publish ./pacts \
334
+ --consumer-app-version=${{ github.sha }} \
335
+ --branch=${{ github.ref_name }} \
336
+ --broker-base-url=${{ secrets.PACT_BROKER_URL }} \
337
+ --broker-token=${{ secrets.PACT_BROKER_TOKEN }}
338
+ ```
339
+
340
+ ## Chaos Testing
341
+
342
+ Intentionally introduce failures to test system resilience.
343
+
344
+ ### Application-Level Chaos
345
+
346
+ ```ts
347
+ // chaos/ChaosProxy.ts
348
+ export class ChaosProxy {
349
+ private target: string;
350
+ private failures: { count: number; status: number } | null = null;
351
+ private latency: number = 0;
352
+ private requestCount: number = 0;
353
+
354
+ constructor(target: string) {
355
+ this.target = target;
356
+ }
357
+
358
+ injectLatency(ms: number): void {
359
+ this.latency = ms;
360
+ }
361
+
362
+ injectFailures(config: { count: number; status: number }): void {
363
+ this.failures = config;
364
+ }
365
+
366
+ async request(options: RequestOptions): Promise<Response> {
367
+ this.requestCount++;
368
+
369
+ // Inject latency
370
+ if (this.latency > 0) {
371
+ await sleep(this.latency);
372
+ }
373
+
374
+ // Inject failures
375
+ if (this.failures && this.failures.count > 0) {
376
+ this.failures.count--;
377
+ return new Response(null, { status: this.failures.status });
378
+ }
379
+
380
+ return fetch(`${this.target}${options.path}`, options);
381
+ }
382
+
383
+ getRequestCount(): number {
384
+ return this.requestCount;
385
+ }
386
+
387
+ restore(): void {
388
+ this.failures = null;
389
+ this.latency = 0;
390
+ this.requestCount = 0;
391
+ }
392
+ }
393
+ ```
394
+
395
+ ### Chaos Testing Examples
396
+
397
+ ```ts
398
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
399
+ import { ChaosProxy } from './chaos/ChaosProxy';
400
+ import { OrderService } from './services/OrderService';
401
+
402
+ describe('Order Service Resilience', () => {
403
+ let paymentChaos: ChaosProxy;
404
+ let inventoryChaos: ChaosProxy;
405
+ let orderService: OrderService;
406
+
407
+ beforeEach(() => {
408
+ paymentChaos = new ChaosProxy('http://payment-service');
409
+ inventoryChaos = new ChaosProxy('http://inventory-service');
410
+ orderService = new OrderService({
411
+ paymentClient: paymentChaos,
412
+ inventoryClient: inventoryChaos,
413
+ });
414
+ });
415
+
416
+ afterEach(() => {
417
+ paymentChaos.restore();
418
+ inventoryChaos.restore();
419
+ });
420
+
421
+ it('retries on transient payment failures', async () => {
422
+ // First 2 requests fail, then succeed
423
+ paymentChaos.injectFailures({ count: 2, status: 503 });
424
+
425
+ const result = await orderService.processPayment({
426
+ orderId: '123',
427
+ amount: 100,
428
+ });
429
+
430
+ expect(result.success).toBe(true);
431
+ expect(paymentChaos.getRequestCount()).toBe(3);
432
+ });
433
+
434
+ it('handles payment timeout gracefully', async () => {
435
+ paymentChaos.injectLatency(5000);
436
+
437
+ const result = await orderService.processPayment({
438
+ orderId: '123',
439
+ amount: 100,
440
+ });
441
+
442
+ expect(result.success).toBe(false);
443
+ expect(result.error).toBe('payment_timeout');
444
+ expect(result.retryable).toBe(true);
445
+ });
446
+
447
+ it('falls back when inventory service is down', async () => {
448
+ inventoryChaos.injectFailures({ count: 10, status: 500 });
449
+
450
+ const result = await orderService.checkInventory('product-1');
451
+
452
+ // Should use cached data or pessimistic default
453
+ expect(result.available).toBe(false);
454
+ expect(result.source).toBe('fallback');
455
+ });
456
+
457
+ it('circuit breaker opens after repeated failures', async () => {
458
+ paymentChaos.injectFailures({ count: 100, status: 500 });
459
+
460
+ // Make multiple requests to trip circuit breaker
461
+ for (let i = 0; i < 10; i++) {
462
+ await orderService.processPayment({ orderId: `${i}`, amount: 100 });
463
+ }
464
+
465
+ // Circuit should be open now
466
+ const result = await orderService.processPayment({
467
+ orderId: '999',
468
+ amount: 100,
469
+ });
470
+
471
+ expect(result.error).toBe('circuit_open');
472
+ // Shouldn't have made another actual request
473
+ expect(paymentChaos.getRequestCount()).toBeLessThan(15);
474
+ });
475
+ });
476
+ ```
477
+
478
+ ### Infrastructure Chaos
479
+
480
+ Using Chaos Mesh in Kubernetes:
481
+
482
+ ```yaml
483
+ # chaos/network-delay.yaml
484
+ apiVersion: chaos-mesh.org/v1alpha1
485
+ kind: NetworkChaos
486
+ metadata:
487
+ name: payment-delay
488
+ spec:
489
+ action: delay
490
+ mode: all
491
+ selector:
492
+ namespaces:
493
+ - production
494
+ labelSelectors:
495
+ app: payment-service
496
+ delay:
497
+ latency: "500ms"
498
+ jitter: "100ms"
499
+ duration: "5m"
500
+ ```
501
+
502
+ ```ts
503
+ // Test that runs chaos experiment
504
+ import { execSync } from 'child_process';
505
+
506
+ describe('Production Resilience', () => {
507
+ it('handles payment service latency', async () => {
508
+ // Apply chaos
509
+ execSync('kubectl apply -f chaos/network-delay.yaml');
510
+
511
+ try {
512
+ // Run actual load test
513
+ const results = await runLoadTest({
514
+ scenario: 'checkout',
515
+ duration: '5m',
516
+ vus: 50,
517
+ });
518
+
519
+ // Verify SLOs are maintained
520
+ expect(results.errorRate).toBeLessThan(0.05);
521
+ expect(results.p95Latency).toBeLessThan(3000);
522
+ } finally {
523
+ // Remove chaos
524
+ execSync('kubectl delete -f chaos/network-delay.yaml');
525
+ }
526
+ });
527
+ });
528
+ ```
529
+
530
+ ## Fuzzing
531
+
532
+ Automated testing with random/malformed inputs.
533
+
534
+ ```ts
535
+ // Using a fuzzing library
536
+ import { fuzz } from '@jazzer.js/core';
537
+
538
+ fuzz('processUserInput', (data: Uint8Array) => {
539
+ const input = new TextDecoder().decode(data);
540
+
541
+ // Should never throw unexpected errors
542
+ try {
543
+ const result = processUserInput(input);
544
+ // If it returns, it should be valid
545
+ expect(result).toBeDefined();
546
+ } catch (error) {
547
+ // Only expected errors allowed
548
+ expect(error).toBeInstanceOf(ValidationError);
549
+ }
550
+ });
551
+
552
+ // API fuzzing
553
+ describe('API Fuzzing', () => {
554
+ it.prop([fc.json()])('handles arbitrary JSON', async (json) => {
555
+ const response = await request(app)
556
+ .post('/api/data')
557
+ .send(json);
558
+
559
+ // Should never crash
560
+ expect([200, 400, 422]).toContain(response.status);
561
+ });
562
+
563
+ it.prop([fc.string()])('handles arbitrary strings in name', async (name) => {
564
+ const response = await request(app)
565
+ .post('/api/users')
566
+ .send({ name, email: 'test@example.com' });
567
+
568
+ // Should validate, not crash
569
+ expect([201, 400, 422]).toContain(response.status);
570
+ });
571
+ });
572
+ ```
573
+
574
+ ## Combining Techniques
575
+
576
+ ```ts
577
+ describe('OrderService Comprehensive Tests', () => {
578
+ // Property-based: Test invariants
579
+ it.prop([orderArbitrary])('order total is never negative', (order) => {
580
+ expect(calculateTotal(order)).toBeGreaterThanOrEqual(0);
581
+ });
582
+
583
+ // Mutation testing catches weak tests automatically
584
+
585
+ // Contract: Verify integration points
586
+ it('adheres to payment service contract', async () => {
587
+ await provider.executeTest(/* ... */);
588
+ });
589
+
590
+ // Chaos: Test resilience
591
+ it('handles payment failures gracefully', async () => {
592
+ paymentChaos.injectFailures({ count: 2, status: 503 });
593
+ // ...
594
+ });
595
+ });
596
+ ```