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.
- package/README.md +69 -5
- package/bin/cli.js +4 -1
- package/package.json +8 -3
- package/src/index.js +776 -134
- 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,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
|
+
```
|