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,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
|