omgkit 2.21.7 → 2.22.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/package.json +13 -2
- package/plugin/commands/omgdeploy/edge.md +1 -1
- package/plugin/commands/quality/test-chaos.md +99 -0
- package/plugin/commands/quality/test-mutate.md +65 -0
- package/plugin/commands/quality/test-omega.md +61 -0
- package/plugin/commands/quality/test-performance.md +82 -0
- package/plugin/commands/quality/test-property.md +61 -0
- package/plugin/commands/quality/test-security.md +76 -0
- package/plugin/skills/testing/chaos-testing/SKILL.md +389 -0
- package/plugin/skills/testing/comprehensive-testing/SKILL.md +248 -0
- package/plugin/skills/testing/mutation-testing/SKILL.md +335 -0
- package/plugin/skills/testing/performance-testing/SKILL.md +361 -0
- package/plugin/skills/testing/property-testing/SKILL.md +341 -0
- package/plugin/skills/testing/security-testing/SKILL.md +347 -0
- package/plugin/workflows/testing/comprehensive-testing.md +130 -0
- package/plugin/workflows/testing/security-hardening.md +196 -0
- package/plugin/workflows/testing/test-driven-development.md +181 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing/mutation-testing
|
|
3
|
+
description: Mutation testing with Stryker to verify test quality by introducing code mutations and measuring detection rates
|
|
4
|
+
category: testing
|
|
5
|
+
tags:
|
|
6
|
+
- testing
|
|
7
|
+
- mutation
|
|
8
|
+
- stryker
|
|
9
|
+
- quality
|
|
10
|
+
- coverage
|
|
11
|
+
related_skills:
|
|
12
|
+
- testing/comprehensive-testing
|
|
13
|
+
- testing/vitest
|
|
14
|
+
- methodology/quality-gates
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Mutation Testing
|
|
18
|
+
|
|
19
|
+
Measure test quality by introducing bugs (mutations) and verifying your tests catch them.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Install Stryker
|
|
25
|
+
npm install -D @stryker-mutator/core @stryker-mutator/vitest-runner
|
|
26
|
+
|
|
27
|
+
# Initialize configuration
|
|
28
|
+
npx stryker init
|
|
29
|
+
|
|
30
|
+
# Run mutation testing
|
|
31
|
+
npm run test:mutation
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Core Concept
|
|
35
|
+
|
|
36
|
+
Mutation testing introduces small changes (mutations) to your code and runs your tests. If tests still pass, they're too weak.
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
// Original code
|
|
40
|
+
function isAdult(age) {
|
|
41
|
+
return age >= 18;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Mutations Stryker creates:
|
|
45
|
+
function isAdult(age) { return age > 18; } // >= to >
|
|
46
|
+
function isAdult(age) { return age <= 18; } // >= to <=
|
|
47
|
+
function isAdult(age) { return false; } // return false
|
|
48
|
+
function isAdult(age) { return true; } // return true
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A good test catches all mutations:
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
describe('isAdult', () => {
|
|
55
|
+
it('returns true for age 18', () => {
|
|
56
|
+
expect(isAdult(18)).toBe(true); // Catches >= to >
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns false for age 17', () => {
|
|
60
|
+
expect(isAdult(17)).toBe(false); // Catches >= to <=
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns true for age 100', () => {
|
|
64
|
+
expect(isAdult(100)).toBe(true); // Catches return false
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
### stryker.config.json
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"$schema": "https://raw.githubusercontent.com/stryker-mutator/stryker/master/packages/core/schema/stryker-schema.json",
|
|
76
|
+
"packageManager": "npm",
|
|
77
|
+
"testRunner": "vitest",
|
|
78
|
+
"mutate": [
|
|
79
|
+
"src/**/*.js",
|
|
80
|
+
"src/**/*.ts",
|
|
81
|
+
"!src/**/*.test.js",
|
|
82
|
+
"!src/**/*.spec.ts"
|
|
83
|
+
],
|
|
84
|
+
"reporters": [
|
|
85
|
+
"progress",
|
|
86
|
+
"clear-text",
|
|
87
|
+
"html",
|
|
88
|
+
"json"
|
|
89
|
+
],
|
|
90
|
+
"htmlReporter": {
|
|
91
|
+
"fileName": "reports/mutation/index.html"
|
|
92
|
+
},
|
|
93
|
+
"thresholds": {
|
|
94
|
+
"high": 80,
|
|
95
|
+
"low": 60,
|
|
96
|
+
"break": 50
|
|
97
|
+
},
|
|
98
|
+
"concurrency": 4,
|
|
99
|
+
"timeoutMS": 60000
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Mutation Operators
|
|
104
|
+
|
|
105
|
+
### Arithmetic Operators
|
|
106
|
+
```javascript
|
|
107
|
+
// Original: a + b
|
|
108
|
+
a - b // Plus to Minus
|
|
109
|
+
a * b // Plus to Times
|
|
110
|
+
a / b // Plus to Divide
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Comparison Operators
|
|
114
|
+
```javascript
|
|
115
|
+
// Original: a > b
|
|
116
|
+
a >= b // Greater to GreaterOrEqual
|
|
117
|
+
a < b // Greater to Less
|
|
118
|
+
a <= b // Greater to LessOrEqual
|
|
119
|
+
a == b // Greater to Equal
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Logical Operators
|
|
123
|
+
```javascript
|
|
124
|
+
// Original: a && b
|
|
125
|
+
a || b // And to Or
|
|
126
|
+
|
|
127
|
+
// Original: !a
|
|
128
|
+
a // Negate removal
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Boundary Mutations
|
|
132
|
+
```javascript
|
|
133
|
+
// Original: i < 10
|
|
134
|
+
i <= 10 // Less to LessOrEqual
|
|
135
|
+
i < 11 // Boundary change
|
|
136
|
+
i < 9 // Boundary change
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Return Value Mutations
|
|
140
|
+
```javascript
|
|
141
|
+
// Original: return value
|
|
142
|
+
return undefined; // Remove return
|
|
143
|
+
return !value; // Negate return
|
|
144
|
+
return ""; // Empty string
|
|
145
|
+
return 0; // Zero
|
|
146
|
+
return null; // Null
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Understanding Results
|
|
150
|
+
|
|
151
|
+
### Mutation States
|
|
152
|
+
|
|
153
|
+
| State | Description | Action |
|
|
154
|
+
|-------|-------------|--------|
|
|
155
|
+
| **Killed** | Test failed = mutation caught | Good! |
|
|
156
|
+
| **Survived** | Tests passed = mutation missed | Add tests |
|
|
157
|
+
| **Timeout** | Tests took too long | Check infinite loops |
|
|
158
|
+
| **No Coverage** | No tests cover this code | Add tests |
|
|
159
|
+
| **Compile Error** | Mutation broke compilation | Ignore |
|
|
160
|
+
|
|
161
|
+
### Mutation Score
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
Mutation Score = (Killed / Total) * 100%
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- **80%+**: Excellent test quality
|
|
168
|
+
- **60-80%**: Good, room for improvement
|
|
169
|
+
- **40-60%**: Weak tests, many gaps
|
|
170
|
+
- **<40%**: Critical test deficiency
|
|
171
|
+
|
|
172
|
+
## Improving Mutation Score
|
|
173
|
+
|
|
174
|
+
### 1. Boundary Testing
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
// Weak: Only tests middle values
|
|
178
|
+
it('validates age', () => {
|
|
179
|
+
expect(isValidAge(25)).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Strong: Tests boundaries
|
|
183
|
+
it('validates age boundaries', () => {
|
|
184
|
+
expect(isValidAge(0)).toBe(true); // Min boundary
|
|
185
|
+
expect(isValidAge(-1)).toBe(false); // Below min
|
|
186
|
+
expect(isValidAge(150)).toBe(true); // Max boundary
|
|
187
|
+
expect(isValidAge(151)).toBe(false); // Above max
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 2. Condition Coverage
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
// Original
|
|
195
|
+
function process(a, b) {
|
|
196
|
+
if (a > 0 && b > 0) {
|
|
197
|
+
return 'both positive';
|
|
198
|
+
}
|
|
199
|
+
return 'not both positive';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Weak: Only tests one path
|
|
203
|
+
it('processes positive', () => {
|
|
204
|
+
expect(process(1, 1)).toBe('both positive');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Strong: Tests all conditions
|
|
208
|
+
it('processes various combinations', () => {
|
|
209
|
+
expect(process(1, 1)).toBe('both positive');
|
|
210
|
+
expect(process(-1, 1)).toBe('not both positive'); // a negative
|
|
211
|
+
expect(process(1, -1)).toBe('not both positive'); // b negative
|
|
212
|
+
expect(process(0, 1)).toBe('not both positive'); // a zero
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 3. Return Value Testing
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
// Weak: Only tests truthy
|
|
220
|
+
it('checks admin', () => {
|
|
221
|
+
expect(isAdmin(adminUser)).toBeTruthy();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Strong: Tests exact values
|
|
225
|
+
it('checks admin status', () => {
|
|
226
|
+
expect(isAdmin(adminUser)).toBe(true);
|
|
227
|
+
expect(isAdmin(regularUser)).toBe(false);
|
|
228
|
+
expect(isAdmin(null)).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Incremental Mutation Testing
|
|
233
|
+
|
|
234
|
+
For large codebases, run mutations on changed files only:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# Only mutate changed files
|
|
238
|
+
git diff --name-only origin/main | xargs npx stryker run --mutate
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## CI Integration
|
|
242
|
+
|
|
243
|
+
### GitHub Actions
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
name: Mutation Testing
|
|
247
|
+
|
|
248
|
+
on:
|
|
249
|
+
push:
|
|
250
|
+
branches: [main]
|
|
251
|
+
pull_request:
|
|
252
|
+
|
|
253
|
+
jobs:
|
|
254
|
+
mutation:
|
|
255
|
+
runs-on: ubuntu-latest
|
|
256
|
+
steps:
|
|
257
|
+
- uses: actions/checkout@v4
|
|
258
|
+
|
|
259
|
+
- name: Setup Node.js
|
|
260
|
+
uses: actions/setup-node@v4
|
|
261
|
+
with:
|
|
262
|
+
node-version: '20'
|
|
263
|
+
cache: 'npm'
|
|
264
|
+
|
|
265
|
+
- run: npm ci
|
|
266
|
+
|
|
267
|
+
- name: Run Mutation Tests
|
|
268
|
+
run: npm run test:mutation
|
|
269
|
+
|
|
270
|
+
- name: Upload Report
|
|
271
|
+
uses: actions/upload-artifact@v4
|
|
272
|
+
with:
|
|
273
|
+
name: mutation-report
|
|
274
|
+
path: reports/mutation/
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Performance Optimization
|
|
278
|
+
|
|
279
|
+
### Reduce Mutation Scope
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
{
|
|
283
|
+
"mutate": [
|
|
284
|
+
"src/core/**/*.js",
|
|
285
|
+
"!src/core/**/*.test.js",
|
|
286
|
+
"!src/core/generated/**"
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Increase Parallelism
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"concurrency": 8,
|
|
296
|
+
"testRunner": "vitest"
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Filter Mutators
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"mutator": {
|
|
305
|
+
"excludedMutations": [
|
|
306
|
+
"StringLiteral",
|
|
307
|
+
"ObjectLiteral"
|
|
308
|
+
]
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## When to Use
|
|
314
|
+
|
|
315
|
+
### Good Candidates
|
|
316
|
+
- Critical business logic
|
|
317
|
+
- Security-sensitive code
|
|
318
|
+
- Mathematical calculations
|
|
319
|
+
- State machines
|
|
320
|
+
- Validation logic
|
|
321
|
+
|
|
322
|
+
### When to Skip
|
|
323
|
+
- Generated code
|
|
324
|
+
- Configuration files
|
|
325
|
+
- Third-party wrappers
|
|
326
|
+
- UI components
|
|
327
|
+
- Test utilities
|
|
328
|
+
|
|
329
|
+
## Anti-Patterns
|
|
330
|
+
|
|
331
|
+
1. **Chasing 100%**: Diminishing returns above 90%
|
|
332
|
+
2. **Ignoring Timeouts**: Fix infinite loop mutations
|
|
333
|
+
3. **Testing Everything**: Focus on critical paths
|
|
334
|
+
4. **No Baseline**: Establish baseline before improving
|
|
335
|
+
5. **Infrequent Runs**: Run on every PR
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing/performance-testing
|
|
3
|
+
description: Performance testing patterns including load testing, stress testing, benchmarking, and profiling for optimal application performance
|
|
4
|
+
category: testing
|
|
5
|
+
tags:
|
|
6
|
+
- testing
|
|
7
|
+
- performance
|
|
8
|
+
- benchmarking
|
|
9
|
+
- load-testing
|
|
10
|
+
- profiling
|
|
11
|
+
related_skills:
|
|
12
|
+
- testing/comprehensive-testing
|
|
13
|
+
- devops/observability
|
|
14
|
+
- backend/caching-strategies
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Performance Testing
|
|
18
|
+
|
|
19
|
+
Comprehensive performance testing for speed, scalability, and resource efficiency.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Run benchmarks
|
|
25
|
+
npm run test:performance
|
|
26
|
+
|
|
27
|
+
# Profile with Node.js
|
|
28
|
+
node --prof app.js
|
|
29
|
+
node --prof-process isolate-*.log > profile.txt
|
|
30
|
+
|
|
31
|
+
# Load testing with k6
|
|
32
|
+
k6 run load-test.js
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Types of Performance Tests
|
|
36
|
+
|
|
37
|
+
### 1. Benchmark Tests
|
|
38
|
+
|
|
39
|
+
Measure execution time of specific operations.
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
import { describe, it, expect } from 'vitest';
|
|
43
|
+
|
|
44
|
+
describe('Performance Benchmarks', () => {
|
|
45
|
+
function timeExecution(fn, iterations = 1000) {
|
|
46
|
+
const start = performance.now();
|
|
47
|
+
for (let i = 0; i < iterations; i++) {
|
|
48
|
+
fn();
|
|
49
|
+
}
|
|
50
|
+
const end = performance.now();
|
|
51
|
+
return (end - start) / iterations;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
it('sorts 1000 items under 1ms', () => {
|
|
55
|
+
const data = Array(1000).fill().map(() => Math.random());
|
|
56
|
+
const time = timeExecution(() => [...data].sort((a, b) => a - b), 100);
|
|
57
|
+
expect(time).toBeLessThan(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('hashes string under 0.1ms', () => {
|
|
61
|
+
const time = timeExecution(() => hashString('test-string'), 10000);
|
|
62
|
+
expect(time).toBeLessThan(0.1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('parses JSON under 0.5ms', () => {
|
|
66
|
+
const json = JSON.stringify({ users: Array(100).fill({ name: 'test' }) });
|
|
67
|
+
const time = timeExecution(() => JSON.parse(json), 1000);
|
|
68
|
+
expect(time).toBeLessThan(0.5);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. Load Tests
|
|
74
|
+
|
|
75
|
+
Simulate concurrent users to test scalability.
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
// k6 load test script
|
|
79
|
+
import http from 'k6/http';
|
|
80
|
+
import { check, sleep } from 'k6';
|
|
81
|
+
|
|
82
|
+
export const options = {
|
|
83
|
+
stages: [
|
|
84
|
+
{ duration: '1m', target: 50 }, // Ramp up to 50 users
|
|
85
|
+
{ duration: '3m', target: 50 }, // Stay at 50 users
|
|
86
|
+
{ duration: '1m', target: 100 }, // Ramp up to 100
|
|
87
|
+
{ duration: '3m', target: 100 }, // Stay at 100
|
|
88
|
+
{ duration: '1m', target: 0 }, // Ramp down
|
|
89
|
+
],
|
|
90
|
+
thresholds: {
|
|
91
|
+
http_req_duration: ['p(95)<200'], // 95% under 200ms
|
|
92
|
+
http_req_failed: ['rate<0.01'], // Error rate < 1%
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default function () {
|
|
97
|
+
const res = http.get('https://api.example.com/users');
|
|
98
|
+
|
|
99
|
+
check(res, {
|
|
100
|
+
'status is 200': (r) => r.status === 200,
|
|
101
|
+
'response time < 200ms': (r) => r.timings.duration < 200,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
sleep(1);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Stress Tests
|
|
109
|
+
|
|
110
|
+
Push system beyond normal limits.
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
describe('Stress Tests', () => {
|
|
114
|
+
it('handles 10000 concurrent connections', async () => {
|
|
115
|
+
const connections = Array(10000).fill().map(async () => {
|
|
116
|
+
const ws = new WebSocket('ws://localhost:3000');
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
ws.onopen = () => resolve(ws);
|
|
119
|
+
ws.onerror = reject;
|
|
120
|
+
setTimeout(() => reject(new Error('timeout')), 5000);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const results = await Promise.allSettled(connections);
|
|
125
|
+
const successful = results.filter(r => r.status === 'fulfilled');
|
|
126
|
+
expect(successful.length).toBeGreaterThan(9000);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('recovers after memory pressure', async () => {
|
|
130
|
+
const allocations = [];
|
|
131
|
+
|
|
132
|
+
// Allocate memory
|
|
133
|
+
for (let i = 0; i < 100; i++) {
|
|
134
|
+
allocations.push(new Array(1000000).fill('x'));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Force GC and clear
|
|
138
|
+
allocations.length = 0;
|
|
139
|
+
if (global.gc) global.gc();
|
|
140
|
+
|
|
141
|
+
// System should still function
|
|
142
|
+
const result = await api.get('/health');
|
|
143
|
+
expect(result.status).toBe(200);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 4. Memory Tests
|
|
149
|
+
|
|
150
|
+
Detect memory leaks and inefficiencies.
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
describe('Memory Tests', () => {
|
|
154
|
+
it('does not leak memory on repeated operations', async () => {
|
|
155
|
+
const before = process.memoryUsage().heapUsed;
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < 10000; i++) {
|
|
158
|
+
await processRequest({ data: 'test' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (global.gc) global.gc();
|
|
162
|
+
await new Promise(r => setTimeout(r, 100));
|
|
163
|
+
|
|
164
|
+
const after = process.memoryUsage().heapUsed;
|
|
165
|
+
const leaked = after - before;
|
|
166
|
+
|
|
167
|
+
expect(leaked).toBeLessThan(10 * 1024 * 1024); // 10MB
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('releases event listeners', () => {
|
|
171
|
+
const emitter = new EventEmitter();
|
|
172
|
+
const before = emitter.listenerCount('event');
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < 100; i++) {
|
|
175
|
+
const handler = () => {};
|
|
176
|
+
emitter.on('event', handler);
|
|
177
|
+
emitter.off('event', handler);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const after = emitter.listenerCount('event');
|
|
181
|
+
expect(after).toBe(before);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## API Response Time Tests
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
describe('API Performance', () => {
|
|
190
|
+
const SLA = {
|
|
191
|
+
p50: 50, // 50th percentile under 50ms
|
|
192
|
+
p95: 200, // 95th percentile under 200ms
|
|
193
|
+
p99: 500, // 99th percentile under 500ms
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
it('meets response time SLA', async () => {
|
|
197
|
+
const times = [];
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < 1000; i++) {
|
|
200
|
+
const start = performance.now();
|
|
201
|
+
await api.get('/users');
|
|
202
|
+
times.push(performance.now() - start);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
times.sort((a, b) => a - b);
|
|
206
|
+
|
|
207
|
+
const p50 = times[Math.floor(times.length * 0.50)];
|
|
208
|
+
const p95 = times[Math.floor(times.length * 0.95)];
|
|
209
|
+
const p99 = times[Math.floor(times.length * 0.99)];
|
|
210
|
+
|
|
211
|
+
expect(p50).toBeLessThan(SLA.p50);
|
|
212
|
+
expect(p95).toBeLessThan(SLA.p95);
|
|
213
|
+
expect(p99).toBeLessThan(SLA.p99);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Database Performance Tests
|
|
219
|
+
|
|
220
|
+
```javascript
|
|
221
|
+
describe('Database Performance', () => {
|
|
222
|
+
it('query executes under 10ms', async () => {
|
|
223
|
+
const start = performance.now();
|
|
224
|
+
await db.query('SELECT * FROM users WHERE id = ?', [1]);
|
|
225
|
+
const duration = performance.now() - start;
|
|
226
|
+
expect(duration).toBeLessThan(10);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('handles 100 concurrent queries', async () => {
|
|
230
|
+
const queries = Array(100).fill().map(() =>
|
|
231
|
+
db.query('SELECT * FROM users LIMIT 10')
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const start = performance.now();
|
|
235
|
+
await Promise.all(queries);
|
|
236
|
+
const duration = performance.now() - start;
|
|
237
|
+
|
|
238
|
+
expect(duration).toBeLessThan(1000);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('insert batch is efficient', async () => {
|
|
242
|
+
const records = Array(1000).fill().map((_, i) => ({
|
|
243
|
+
name: `User ${i}`,
|
|
244
|
+
email: `user${i}@test.com`,
|
|
245
|
+
}));
|
|
246
|
+
|
|
247
|
+
const start = performance.now();
|
|
248
|
+
await db.batchInsert('users', records);
|
|
249
|
+
const duration = performance.now() - start;
|
|
250
|
+
|
|
251
|
+
expect(duration).toBeLessThan(500);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Profiling Patterns
|
|
257
|
+
|
|
258
|
+
### CPU Profiling
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
const v8Profiler = require('v8-profiler-next');
|
|
262
|
+
|
|
263
|
+
async function profileCPU(fn, name) {
|
|
264
|
+
v8Profiler.startProfiling(name);
|
|
265
|
+
|
|
266
|
+
await fn();
|
|
267
|
+
|
|
268
|
+
const profile = v8Profiler.stopProfiling(name);
|
|
269
|
+
profile.export((error, result) => {
|
|
270
|
+
fs.writeFileSync(`${name}.cpuprofile`, result);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Memory Profiling
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
function takeHeapSnapshot(name) {
|
|
279
|
+
const snapshot = v8.writeHeapSnapshot();
|
|
280
|
+
console.log(`Heap snapshot written to ${snapshot}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// In test
|
|
284
|
+
it('memory usage is stable', async () => {
|
|
285
|
+
takeHeapSnapshot('before');
|
|
286
|
+
|
|
287
|
+
for (let i = 0; i < 10000; i++) {
|
|
288
|
+
await processData();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (global.gc) global.gc();
|
|
292
|
+
takeHeapSnapshot('after');
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Performance Budgets
|
|
297
|
+
|
|
298
|
+
```javascript
|
|
299
|
+
const budgets = {
|
|
300
|
+
'First Contentful Paint': 1500,
|
|
301
|
+
'Time to Interactive': 3500,
|
|
302
|
+
'Total Bundle Size': 250 * 1024,
|
|
303
|
+
'API Response Time': 200,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
describe('Performance Budgets', () => {
|
|
307
|
+
it('bundle size within budget', async () => {
|
|
308
|
+
const stats = require('./dist/stats.json');
|
|
309
|
+
const totalSize = stats.assets
|
|
310
|
+
.filter(a => a.name.endsWith('.js'))
|
|
311
|
+
.reduce((sum, a) => sum + a.size, 0);
|
|
312
|
+
|
|
313
|
+
expect(totalSize).toBeLessThan(budgets['Total Bundle Size']);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Continuous Performance Testing
|
|
319
|
+
|
|
320
|
+
### GitHub Actions
|
|
321
|
+
|
|
322
|
+
```yaml
|
|
323
|
+
name: Performance Tests
|
|
324
|
+
|
|
325
|
+
on:
|
|
326
|
+
push:
|
|
327
|
+
branches: [main]
|
|
328
|
+
pull_request:
|
|
329
|
+
|
|
330
|
+
jobs:
|
|
331
|
+
performance:
|
|
332
|
+
runs-on: ubuntu-latest
|
|
333
|
+
steps:
|
|
334
|
+
- uses: actions/checkout@v4
|
|
335
|
+
- uses: actions/setup-node@v4
|
|
336
|
+
with:
|
|
337
|
+
node-version: '20'
|
|
338
|
+
|
|
339
|
+
- run: npm ci
|
|
340
|
+
- run: npm run test:performance
|
|
341
|
+
|
|
342
|
+
- name: Compare with baseline
|
|
343
|
+
run: |
|
|
344
|
+
npm run benchmark:compare -- --baseline main
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## When to Use
|
|
348
|
+
|
|
349
|
+
- Before major releases
|
|
350
|
+
- After performance-critical changes
|
|
351
|
+
- During capacity planning
|
|
352
|
+
- When SLA monitoring shows degradation
|
|
353
|
+
- As part of CI/CD pipeline
|
|
354
|
+
|
|
355
|
+
## Anti-Patterns
|
|
356
|
+
|
|
357
|
+
1. **Testing on Development Hardware**: Use production-like environment
|
|
358
|
+
2. **Ignoring Warmup**: JIT compilation affects first runs
|
|
359
|
+
3. **Single Metric Focus**: Monitor multiple dimensions
|
|
360
|
+
4. **No Baseline**: Always compare against baseline
|
|
361
|
+
5. **Flaky Thresholds**: Use percentiles, not averages
|