perf-skills 1.0.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/LICENSE.md +8 -0
- package/README.md +227 -0
- package/package.json +42 -0
- package/perf-skills/.claude-plugin/plugin.json +5 -0
- package/perf-skills/SKILL.md +169 -0
- package/perf-skills/references/tools/gatling.md +286 -0
- package/perf-skills/references/tools/jmeter.md +251 -0
- package/perf-skills/references/tools/k6.md +269 -0
- package/perf-skills/references/tools/loadrunner.md +116 -0
- package/perf-skills/references/tools/locust.md +133 -0
- package/perf-skills/references/tools/neoload.md +61 -0
- package/perf-skills/references/tools/octoperf.md +50 -0
- package/perf-skills/references/topics/database-testing.md +214 -0
- package/perf-skills/references/topics/modern-architectures.md +314 -0
- package/perf-skills/references/topics/observability.md +201 -0
- package/perf-skills/references/topics/production-testing.md +214 -0
- package/perf-skills/references/topics/protocol-testing.md +274 -0
- package/perf-skills/references/topics/results-analysis.md +244 -0
- package/perf-skills/references/topics/script-generation.md +221 -0
- package/perf-skills/references/topics/test-data.md +196 -0
- package/perf-skills/references/topics/test-execution.md +281 -0
- package/perf-skills/references/topics/workload-design.md +207 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Results Analysis
|
|
2
|
+
|
|
3
|
+
Interpreting performance test results is a skill in itself. Raw numbers are meaningless without context — this reference covers how to read, analyze, and act on performance data.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Core Metrics
|
|
8
|
+
|
|
9
|
+
| Metric | Description | Target |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **Throughput** | Requests per second (RPS / TPS) | ≥ defined baseline |
|
|
12
|
+
| **Response Time (p50)** | Median — half of requests faster than this | Per SLA |
|
|
13
|
+
| **Response Time (p95)** | 95% of requests faster than this — main SLA metric | Per SLA |
|
|
14
|
+
| **Response Time (p99)** | Tail latency — reveals worst-case behavior | Per SLA |
|
|
15
|
+
| **Error Rate** | % of failed requests | < 1% (or as defined) |
|
|
16
|
+
| **Active VUs / Concurrency** | Number of simulated users at any point | Matches load profile |
|
|
17
|
+
| **Network I/O** | Bytes/sec in and out — detects bandwidth bottlenecks | Headroom vs NIC capacity |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Response Time Percentiles
|
|
22
|
+
|
|
23
|
+
Never rely on averages alone. They mask the tail:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Example: 100 requests
|
|
27
|
+
98 requests at 100ms ← average pulled down
|
|
28
|
+
2 requests at 5000ms ← 2% of users wait 5 seconds
|
|
29
|
+
|
|
30
|
+
Average: 198ms ← looks fine
|
|
31
|
+
p95: 100ms ← looks fine
|
|
32
|
+
p99: 5000ms ← alerts you to the problem
|
|
33
|
+
Max: 5100ms ← confirms the problem
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Which percentile to use for SLAs:**
|
|
37
|
+
- p50 = typical user experience
|
|
38
|
+
- p95 = most users' worst case (standard SLA metric)
|
|
39
|
+
- p99 = tail latency (critical for high-volume services)
|
|
40
|
+
- p99.9 = extreme tail (needed for financial/medical systems)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Reading JMeter Reports
|
|
45
|
+
|
|
46
|
+
### Summary Report columns
|
|
47
|
+
| Column | Meaning |
|
|
48
|
+
|---|---|
|
|
49
|
+
| # Samples | Total requests sent |
|
|
50
|
+
| Average | Mean response time (ms) — use sparingly |
|
|
51
|
+
| Min / Max | Absolute floor and ceiling |
|
|
52
|
+
| Std. Dev. | Variance — high deviation = inconsistent behavior |
|
|
53
|
+
| Error % | Percentage of failed requests |
|
|
54
|
+
| Throughput | Requests/second |
|
|
55
|
+
| KB/sec | Network throughput |
|
|
56
|
+
| p90 / p95 / p99 | Percentile response times |
|
|
57
|
+
|
|
58
|
+
### HTML Dashboard
|
|
59
|
+
Generated with `-e -o results/dashboard` flag. Key graphs:
|
|
60
|
+
- **Response Time Over Time** — spot degradation trends
|
|
61
|
+
- **Transactions Per Second** — confirm throughput matches expectation
|
|
62
|
+
- **Response Time Percentiles** — see distribution
|
|
63
|
+
- **Active Threads Over Time** — correlate with response time
|
|
64
|
+
- **Errors Over Time** — when and what errors spike
|
|
65
|
+
|
|
66
|
+
### JTL Analysis (programmatic)
|
|
67
|
+
```python
|
|
68
|
+
import pandas as pd
|
|
69
|
+
|
|
70
|
+
df = pd.read_csv('results.jtl')
|
|
71
|
+
df['elapsed'] = df['elapsed'].astype(int)
|
|
72
|
+
df['success'] = df['success'].astype(bool)
|
|
73
|
+
|
|
74
|
+
# Filter to successful requests only for latency analysis
|
|
75
|
+
success = df[df['success'] == True]
|
|
76
|
+
|
|
77
|
+
print(f"p50: {success['elapsed'].quantile(0.50):.0f}ms")
|
|
78
|
+
print(f"p95: {success['elapsed'].quantile(0.95):.0f}ms")
|
|
79
|
+
print(f"p99: {success['elapsed'].quantile(0.99):.0f}ms")
|
|
80
|
+
print(f"Max: {success['elapsed'].max():.0f}ms")
|
|
81
|
+
print(f"RPS: {len(df) / (df['timeStamp'].max() - df['timeStamp'].min()) * 1000:.1f}")
|
|
82
|
+
print(f"Error rate: {(~df['success']).mean()*100:.2f}%")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Reading k6 Reports
|
|
88
|
+
|
|
89
|
+
### Terminal summary output
|
|
90
|
+
```
|
|
91
|
+
✓ http_req_duration............: avg=182ms min=45ms med=156ms max=2.1s p(90)=312ms p(95)=489ms
|
|
92
|
+
✓ http_req_failed..............: 0.52% ✓ 52 ✗ 9948
|
|
93
|
+
✓ login_errors.................: count=2
|
|
94
|
+
http_reqs.....................: 10000 83.33/s
|
|
95
|
+
iterations....................: 2000 16.67/s
|
|
96
|
+
vus...........................: 100
|
|
97
|
+
vus_max.......................: 100
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Thresholds status:** `✓` = passed, `✗` = failed (non-zero exit code).
|
|
101
|
+
|
|
102
|
+
### k6 JSON output analysis
|
|
103
|
+
```python
|
|
104
|
+
import json
|
|
105
|
+
|
|
106
|
+
with open('results.json') as f:
|
|
107
|
+
for line in f:
|
|
108
|
+
point = json.loads(line)
|
|
109
|
+
if point['type'] == 'Point' and point['metric'] == 'http_req_duration':
|
|
110
|
+
# Process individual data points
|
|
111
|
+
pass
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Bottleneck Identification Framework
|
|
117
|
+
|
|
118
|
+
When you see high response times or errors, use this framework:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
High Response Time / Errors
|
|
122
|
+
│
|
|
123
|
+
├─ Check: Is the error rate high?
|
|
124
|
+
│ ├─ YES → What status codes? (503 = server overloaded, 504 = timeout, 429 = rate limit)
|
|
125
|
+
│ └─ NO → Latency issue, not availability
|
|
126
|
+
│
|
|
127
|
+
├─ Check: Is throughput (RPS) flat-lining below target?
|
|
128
|
+
│ ├─ YES → Server saturated; find the saturated resource
|
|
129
|
+
│ └─ NO → Throughput ok; issue is latency distribution
|
|
130
|
+
│
|
|
131
|
+
├─ Check: Server CPU > 80%?
|
|
132
|
+
│ ├─ YES → CPU bottleneck → profile code, optimize algorithms, scale horizontally
|
|
133
|
+
│ └─ NO → CPU is not the limit
|
|
134
|
+
│
|
|
135
|
+
├─ Check: Database slow query log showing queries > 100ms?
|
|
136
|
+
│ ├─ YES → DB bottleneck → add indexes, optimize queries, connection pool tuning
|
|
137
|
+
│ └─ NO → Look elsewhere
|
|
138
|
+
│
|
|
139
|
+
├─ Check: JVM GC pauses (Java services)?
|
|
140
|
+
│ ├─ YES → Memory tuning, GC algorithm selection, heap sizing
|
|
141
|
+
│ └─ NO → Not GC
|
|
142
|
+
│
|
|
143
|
+
├─ Check: Connection pool exhausted? (connection refused, pool timeout errors)
|
|
144
|
+
│ ├─ YES → Tune pool size, connection timeout, check for connection leaks
|
|
145
|
+
│ └─ NO → Not pool
|
|
146
|
+
│
|
|
147
|
+
└─ Check: Network I/O saturated?
|
|
148
|
+
├─ YES → Response payload too large? Bandwidth ceiling? CDN needed?
|
|
149
|
+
└─ NO → Dig deeper with APM traces
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Analyzing Latency Patterns
|
|
155
|
+
|
|
156
|
+
### Response Time Over Time Shapes
|
|
157
|
+
|
|
158
|
+
**Stable (good):**
|
|
159
|
+
```
|
|
160
|
+
ms │ ~~~~~~~~~~~~~~~~~~~~
|
|
161
|
+
└──────────────────────── Time
|
|
162
|
+
```
|
|
163
|
+
Consistent response time = healthy, stable system.
|
|
164
|
+
|
|
165
|
+
**Degrading (memory leak / connection exhaustion):**
|
|
166
|
+
```
|
|
167
|
+
ms │ ╱‾‾‾
|
|
168
|
+
│ ╱‾‾‾‾‾‾‾‾
|
|
169
|
+
│ ╱‾‾‾‾‾‾‾
|
|
170
|
+
└──────────────────────── Time
|
|
171
|
+
```
|
|
172
|
+
Gradual increase = something is accumulating over time.
|
|
173
|
+
|
|
174
|
+
**Sawtooth (GC or periodic batch job):**
|
|
175
|
+
```
|
|
176
|
+
ms │ ▁▂▃▄▅▆▇█▁▂▃▄▅▆▇█▁▂▃▄▅
|
|
177
|
+
└──────────────────────── Time
|
|
178
|
+
```
|
|
179
|
+
Periodic spikes = GC pauses, scheduled jobs, connection pool refresh.
|
|
180
|
+
|
|
181
|
+
**Cliff (saturation point):**
|
|
182
|
+
```
|
|
183
|
+
ms │ ╱‾‾‾‾‾‾‾
|
|
184
|
+
│ ─────────────
|
|
185
|
+
└──────────────────────── Time
|
|
186
|
+
[normal load] [saturation]
|
|
187
|
+
```
|
|
188
|
+
Sudden jump at a load threshold = system has hit a constraint (thread limit, DB connection pool, queue depth).
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Comparing Runs (Trend Analysis)
|
|
193
|
+
|
|
194
|
+
Always compare the current run against a baseline — never evaluate a run in isolation.
|
|
195
|
+
|
|
196
|
+
| Metric | Run 1 (Baseline) | Run 2 | Delta | Status |
|
|
197
|
+
|---|---|---|---|---|
|
|
198
|
+
| p95 response time | 320ms | 380ms | +18.7% | ⚠️ Regression |
|
|
199
|
+
| Error rate | 0.1% | 0.08% | -20% | ✅ Improved |
|
|
200
|
+
| Throughput | 450 RPS | 480 RPS | +6.7% | ✅ Improved |
|
|
201
|
+
| p99 | 1200ms | 2100ms | +75% | ❌ Major regression |
|
|
202
|
+
|
|
203
|
+
Tools for trend analysis:
|
|
204
|
+
- **Grafana + InfluxDB**: Overlay runs as time-series overlays.
|
|
205
|
+
- **Gatling Enterprise**: Built-in run comparison.
|
|
206
|
+
- **JMeter + Jenkins Performance Plugin**: Trend charts per build.
|
|
207
|
+
- **Custom scripting**: Python/Pandas on JTL files.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Reporting to Stakeholders
|
|
212
|
+
|
|
213
|
+
Structure your report:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
1. EXECUTIVE SUMMARY
|
|
217
|
+
- Test objective and load profile (1 paragraph)
|
|
218
|
+
- Overall result: PASS / FAIL against SLA
|
|
219
|
+
- Top finding (e.g., "p95 exceeded SLA by 40% above 500 VUs")
|
|
220
|
+
|
|
221
|
+
2. TEST PARAMETERS
|
|
222
|
+
- Tool, VU count, duration, environment, data set
|
|
223
|
+
|
|
224
|
+
3. KEY METRICS TABLE
|
|
225
|
+
- Throughput, p50/p95/p99, error rate vs target
|
|
226
|
+
|
|
227
|
+
4. GRAPHS
|
|
228
|
+
- Response time over time
|
|
229
|
+
- Throughput over time
|
|
230
|
+
- Error rate over time
|
|
231
|
+
- Server metrics (CPU, memory) if available
|
|
232
|
+
|
|
233
|
+
5. BOTTLENECK ANALYSIS
|
|
234
|
+
- What degraded, at what load level, probable cause
|
|
235
|
+
|
|
236
|
+
6. RECOMMENDATIONS
|
|
237
|
+
- Specific, actionable items (e.g., "Add index on orders.customer_id")
|
|
238
|
+
- Priority and estimated impact
|
|
239
|
+
|
|
240
|
+
7. APPENDIX
|
|
241
|
+
- Full metrics breakdown per endpoint
|
|
242
|
+
- Error log samples
|
|
243
|
+
- Environment configuration
|
|
244
|
+
```
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Script Generation & Best Practices
|
|
2
|
+
|
|
3
|
+
This reference covers how to write robust, realistic, and maintainable performance test scripts — regardless of tool. Apply these principles to JMeter, k6, Gatling, Locust, or any other tool.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Scripting Workflow
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
1. Understand the flow ← Get sequence from BA, Swagger, or HAR
|
|
11
|
+
2. Record or scaffold ← Record via proxy or write from API docs
|
|
12
|
+
3. Smoke test (1 VU) ← Validate correctness before load
|
|
13
|
+
4. Correlate dynamic values ← Session tokens, CSRF, IDs, ViewState
|
|
14
|
+
5. Parameterize data ← Replace hardcoded values with variables
|
|
15
|
+
6. Add assertions ← Validate response correctness
|
|
16
|
+
7. Add think time ← Make VU behavior realistic
|
|
17
|
+
8. Add error handling ← Handle expected and unexpected failures
|
|
18
|
+
9. Smoke test again ← Confirm script still works after changes
|
|
19
|
+
10. Ramp up gradually ← 5 → 10 → 25 → 50 → 100 VUs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Recording vs. Coding
|
|
25
|
+
|
|
26
|
+
### Recording (proxy-based)
|
|
27
|
+
- **Pros:** Fast, captures real browser behavior, catches all requests (including hidden calls).
|
|
28
|
+
- **Cons:** Captures noise (analytics, CDN, fonts), needs cleanup and correlation.
|
|
29
|
+
- **Best for:** Complex web apps, SPAs, unfamiliar APIs.
|
|
30
|
+
- **Tools:** JMeter HTTP(S) Test Script Recorder, BlazeMeter Chrome Extension, Gatling Recorder, HAR import (OctoPerf, k6).
|
|
31
|
+
|
|
32
|
+
### Coding from Scratch
|
|
33
|
+
- **Pros:** Clean, no recording noise, easier to version control.
|
|
34
|
+
- **Cons:** Slower for complex flows.
|
|
35
|
+
- **Best for:** REST/GraphQL APIs with Swagger/OpenAPI docs.
|
|
36
|
+
|
|
37
|
+
### Hybrid (Recommended)
|
|
38
|
+
Record a HAR file from the browser → import into your tool → clean up and parameterize → enhance with dynamic logic.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Correlation Deep Dive
|
|
43
|
+
|
|
44
|
+
Correlation is extracting a dynamic value from a response and using it in a subsequent request. Missing correlation is the #1 cause of performance script failure.
|
|
45
|
+
|
|
46
|
+
### Values that always need correlation
|
|
47
|
+
- **Session tokens** (JSESSIONID, ASP.NET_SessionId)
|
|
48
|
+
- **Auth tokens** (JWT, OAuth access_token, refresh_token)
|
|
49
|
+
- **CSRF tokens** (`_token`, `authenticity_token`, `__RequestVerificationToken`)
|
|
50
|
+
- **View state** (ASP.NET `__VIEWSTATE`, `__EVENTVALIDATION`)
|
|
51
|
+
- **Resource IDs** (created order ID, cart ID, uploaded file ID)
|
|
52
|
+
- **One-time codes** (OTP, nonce, challenge)
|
|
53
|
+
- **Timestamps/signatures** used in request signing
|
|
54
|
+
|
|
55
|
+
### Correlation debugging approach
|
|
56
|
+
1. Run the script with 1 VU.
|
|
57
|
+
2. Look for HTTP 4xx errors (especially 403 Forbidden, 422 Unprocessable Entity).
|
|
58
|
+
3. Check the request body/headers — is a token missing or stale?
|
|
59
|
+
4. Use the tool's debug output or proxy (Fiddler, Charles) to inspect live traffic.
|
|
60
|
+
5. Identify where the value appears in a *previous* response.
|
|
61
|
+
6. Add an extractor at that response, reference the variable in the failing request.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Assertions (Why They're Non-Negotiable)
|
|
66
|
+
|
|
67
|
+
Without assertions, your test might generate 1000 RPS of 404 responses or empty bodies — and your metrics will look "fine."
|
|
68
|
+
|
|
69
|
+
### What to assert
|
|
70
|
+
- **Status code** — the obvious one; but also check for 200s that contain error bodies.
|
|
71
|
+
- **Response body** — key field exists and has expected value.
|
|
72
|
+
- **Response time** — flag individual responses over SLA as failures.
|
|
73
|
+
- **Response size** — detect truncated or empty bodies.
|
|
74
|
+
- **Content-Type header** — ensure you got JSON, not an HTML error page.
|
|
75
|
+
|
|
76
|
+
### Assertion layering
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Level 1: HTTP status (global, every request)
|
|
80
|
+
Level 2: Body content check (per transaction type)
|
|
81
|
+
Level 3: Business logic check (e.g., balance is non-negative)
|
|
82
|
+
Level 4: Duration assertion (flag outliers per request)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Handling false failures
|
|
86
|
+
- Exclude known retryable errors (rate limits, 503 during scale events) from SLA breach calculation.
|
|
87
|
+
- Add error handling blocks that retry on specific status codes.
|
|
88
|
+
- Log all failures with request/response detail for post-test debugging.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Error Handling
|
|
93
|
+
|
|
94
|
+
### JMeter
|
|
95
|
+
Use **If Controller** + **Retry** or set "Continue" behavior in Thread Group on sampler error.
|
|
96
|
+
|
|
97
|
+
### k6
|
|
98
|
+
```javascript
|
|
99
|
+
import { check } from 'k6';
|
|
100
|
+
import { sleep } from 'k6';
|
|
101
|
+
|
|
102
|
+
const res = http.post(url, body, params);
|
|
103
|
+
if (res.status === 429) {
|
|
104
|
+
sleep(5); // Back off on rate limit
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
check(res, { 'status 200': (r) => r.status === 200 });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Gatling
|
|
111
|
+
```scala
|
|
112
|
+
.exec(http("Submit Order")
|
|
113
|
+
.post("/orders")
|
|
114
|
+
.check(status.in(200, 201, 202)) // Accept multiple valid codes
|
|
115
|
+
.checkIf((response, session) => response.status != 200) {
|
|
116
|
+
jsonPath("$.error").saveAs("errorMessage")
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Locust
|
|
122
|
+
```python
|
|
123
|
+
with self.client.post("/orders", json=payload, catch_response=True) as res:
|
|
124
|
+
if res.status_code == 200:
|
|
125
|
+
res.success()
|
|
126
|
+
elif res.status_code == 429:
|
|
127
|
+
res.success() # Don't count rate limits as failures
|
|
128
|
+
time.sleep(5)
|
|
129
|
+
else:
|
|
130
|
+
res.failure(f"Unexpected {res.status_code}: {res.text[:200]}")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Session Management Patterns
|
|
136
|
+
|
|
137
|
+
### Cookie-based sessions
|
|
138
|
+
Most tools handle cookies automatically. Verify the Cookie Manager is enabled.
|
|
139
|
+
|
|
140
|
+
### Token-based (JWT/OAuth)
|
|
141
|
+
1. Authenticate in `setup()` or `on_start()`.
|
|
142
|
+
2. Store the token in session variables.
|
|
143
|
+
3. Add as Authorization header to subsequent requests.
|
|
144
|
+
4. Handle token expiry — check for 401 responses and re-authenticate.
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
// k6: handle token refresh
|
|
148
|
+
function getToken() {
|
|
149
|
+
const res = http.post('/auth/token', credentials);
|
|
150
|
+
if (res.status !== 200) throw new Error('Auth failed');
|
|
151
|
+
return res.json('access_token');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let token = getToken();
|
|
155
|
+
|
|
156
|
+
export default function () {
|
|
157
|
+
let res = http.get('/api/profile', { headers: { Authorization: `Bearer ${token}` } });
|
|
158
|
+
if (res.status === 401) {
|
|
159
|
+
token = getToken(); // Refresh token
|
|
160
|
+
res = http.get('/api/profile', { headers: { Authorization: `Bearer ${token}` } });
|
|
161
|
+
}
|
|
162
|
+
check(res, { 'profile 200': (r) => r.status === 200 });
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Naming Conventions
|
|
169
|
+
|
|
170
|
+
Consistent request naming makes results readable:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
BAD: "Request 1", "HTTP Request", "sampler_3"
|
|
174
|
+
GOOD: "POST /api/orders", "GET /products/{id}", "Login - Authenticate"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Use naming patterns:
|
|
178
|
+
- REST APIs: `{METHOD} {path}` e.g., `POST /api/v2/orders`
|
|
179
|
+
- Business transactions: `{UserAction} - {Step}` e.g., `Checkout - Submit Payment`
|
|
180
|
+
- Background calls: `BG - {description}` e.g., `BG - Poll Status`
|
|
181
|
+
|
|
182
|
+
For parameterized URLs, use a fixed name:
|
|
183
|
+
```javascript
|
|
184
|
+
// k6
|
|
185
|
+
http.get(`/products/${productId}`, { tags: { name: 'GET /products/{id}' } });
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Script Structure Best Practices
|
|
191
|
+
|
|
192
|
+
### Modularize reusable flows
|
|
193
|
+
Extract common flows (login, logout, navigation) into separate functions/files. Import them in scenarios.
|
|
194
|
+
|
|
195
|
+
### Use config files for environment portability
|
|
196
|
+
```javascript
|
|
197
|
+
// k6 config
|
|
198
|
+
const config = {
|
|
199
|
+
baseUrl: __ENV.BASE_URL || 'https://staging.example.com',
|
|
200
|
+
timeout: '30s',
|
|
201
|
+
headers: { 'X-Test-Run': __ENV.BUILD_ID || 'manual' },
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Version control your scripts
|
|
206
|
+
Treat performance scripts as production code:
|
|
207
|
+
- Git repository alongside application code
|
|
208
|
+
- PR reviews for script changes
|
|
209
|
+
- Tag script versions to match application releases
|
|
210
|
+
- Store results per tag for trend comparison
|
|
211
|
+
|
|
212
|
+
### Script review checklist
|
|
213
|
+
- [ ] No hardcoded URLs, credentials, or IDs
|
|
214
|
+
- [ ] All dynamic values correlated and parameterized
|
|
215
|
+
- [ ] Assertions on every request (minimum: status code)
|
|
216
|
+
- [ ] Think time applied between transactions
|
|
217
|
+
- [ ] Error handling for known failure cases (401, 429, 503)
|
|
218
|
+
- [ ] Request naming follows convention
|
|
219
|
+
- [ ] Single VU smoke test passes cleanly
|
|
220
|
+
- [ ] Script loads test data from external source (CSV, API)
|
|
221
|
+
- [ ] Environment-specific values in config/env vars
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Test Data Strategy
|
|
2
|
+
|
|
3
|
+
Test data is a first-class concern in performance testing. Bad data causes false failures, data collisions, unrealistic server behavior, and post-test cleanup nightmares.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why Test Data Matters
|
|
8
|
+
|
|
9
|
+
- **Authentication tokens** expire — hardcoded tokens fail within minutes.
|
|
10
|
+
- **Unique constraints** (email, username, order number) cause failures at concurrency > 1.
|
|
11
|
+
- **State-dependent flows** (checkout, approval) require data in the right state before the test.
|
|
12
|
+
- **Cache effects** — if 1000 VUs all hit the same product ID, cache hit rates are unrealistically high; spread data across IDs.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Data Strategy by Test Type
|
|
17
|
+
|
|
18
|
+
| Test Type | Data Strategy |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Smoke (1 VU) | Minimal — single known-good user/record |
|
|
21
|
+
| Load (realistic VUs) | Pool of pre-created users/records matching production volume |
|
|
22
|
+
| Stress (pushing limits) | Large pool; no uniqueness conflicts |
|
|
23
|
+
| Soak (hours) | Rotating/recyclable data; handle state cleanup |
|
|
24
|
+
| Spike (burst) | Same as load; ensure pool size exceeds spike VU count |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Data Sources
|
|
29
|
+
|
|
30
|
+
### Strategy 1: Pre-Generated CSV Files
|
|
31
|
+
|
|
32
|
+
Best for: stable, static data (user credentials, product IDs, account numbers).
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
data/
|
|
36
|
+
├── users.csv (username, password, account_id)
|
|
37
|
+
├── products.csv (product_id, sku, price)
|
|
38
|
+
├── accounts.csv (account_number, routing_number, balance)
|
|
39
|
+
└── orders.csv (order_id, status, customer_id)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Sizing:** CSV row count should be ≥ peak VU count to prevent multiple VUs sharing the same row and causing collisions.
|
|
43
|
+
|
|
44
|
+
**JMeter:** `CSV Data Set Config` element
|
|
45
|
+
**k6:** `SharedArray` with `open()` or `papaparse`
|
|
46
|
+
**Gatling:** `csv("users.csv").circular`
|
|
47
|
+
**Locust:** Read CSV in `on_start()` or `__init__.py`
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### Strategy 2: Database-Seeded Data
|
|
52
|
+
|
|
53
|
+
Best for: complex relational state (orders in specific statuses, accounts with balances, workflows pending approval).
|
|
54
|
+
|
|
55
|
+
**Approach:**
|
|
56
|
+
1. Write a seeding script (Python, SQL, or test framework) to create N records in the right state.
|
|
57
|
+
2. Export IDs to CSV for the load tool to consume.
|
|
58
|
+
3. After the test, run a cleanup script.
|
|
59
|
+
|
|
60
|
+
```sql
|
|
61
|
+
-- Seed 5000 users for load test
|
|
62
|
+
INSERT INTO test_users (username, password_hash, tenant_id)
|
|
63
|
+
SELECT
|
|
64
|
+
'loadtest_user_' || generate_series(1, 5000),
|
|
65
|
+
'$2b$12$fixedhash',
|
|
66
|
+
'perf-test-tenant'
|
|
67
|
+
;
|
|
68
|
+
|
|
69
|
+
-- Export to CSV
|
|
70
|
+
COPY (SELECT username, 'testpass123' AS password FROM test_users WHERE username LIKE 'loadtest_%')
|
|
71
|
+
TO '/tmp/users.csv' CSV HEADER;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### Strategy 3: API-Based Data Setup (Setup Hooks)
|
|
77
|
+
|
|
78
|
+
Best for: data that must be in a specific state per VU (unique cart, session, transaction).
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
// k6 setup() — run before VUs start
|
|
82
|
+
export function setup() {
|
|
83
|
+
const orders = [];
|
|
84
|
+
for (let i = 0; i < 200; i++) {
|
|
85
|
+
const res = http.post('https://api.example.com/admin/orders', JSON.stringify({
|
|
86
|
+
status: 'pending',
|
|
87
|
+
amount: Math.random() * 1000
|
|
88
|
+
}), { headers: adminHeaders });
|
|
89
|
+
orders.push(res.json('orderId'));
|
|
90
|
+
}
|
|
91
|
+
return { orders }; // Passed to default() as data argument
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# Locust on_start()
|
|
97
|
+
def on_start(self):
|
|
98
|
+
res = self.client.post("/api/checkout/initiate", json={"cartId": new_uuid()})
|
|
99
|
+
self.checkout_id = res.json()["checkoutId"]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### Strategy 4: Faker / Synthetic Data Generation
|
|
105
|
+
|
|
106
|
+
Best for: registration flows, profile creation, form submission — where each VU needs unique PII-like data.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# Python (Locust or data generation script)
|
|
110
|
+
from faker import Faker
|
|
111
|
+
fake = Faker()
|
|
112
|
+
|
|
113
|
+
user = {
|
|
114
|
+
"name": fake.name(),
|
|
115
|
+
"email": fake.unique.email(),
|
|
116
|
+
"address": fake.address(),
|
|
117
|
+
"phone": fake.phone_number()
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
// k6 with randomString
|
|
123
|
+
import { randomString, randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js';
|
|
124
|
+
|
|
125
|
+
const email = `user_${randomString(8)}@loadtest.example.com`;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```groovy
|
|
129
|
+
// JMeter JSR223
|
|
130
|
+
import java.util.UUID
|
|
131
|
+
vars.put("email", "user_${UUID.randomUUID()}@loadtest.example.com")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Data Isolation Patterns
|
|
137
|
+
|
|
138
|
+
### Tenant Isolation
|
|
139
|
+
Create a dedicated `perf-test` tenant in multi-tenant systems. This prevents test data from polluting other tenants and enables easy cleanup.
|
|
140
|
+
|
|
141
|
+
### Test Data Tagging
|
|
142
|
+
Tag all test records (e.g., `source: "perf-test"`, `env: "load-test"`) so they can be identified and deleted post-test.
|
|
143
|
+
|
|
144
|
+
### Ephemeral Environments
|
|
145
|
+
In cloud-native architectures, spin up a fresh environment for each test run (Infrastructure as Code), then destroy it. Eliminates test data pollution entirely.
|
|
146
|
+
|
|
147
|
+
### Data Reset Hooks
|
|
148
|
+
Register teardown hooks to delete or reset test data after the run:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# Locust teardown
|
|
152
|
+
@events.test_stop.add_listener
|
|
153
|
+
def cleanup(environment, **kwargs):
|
|
154
|
+
requests.delete(
|
|
155
|
+
f"{HOST}/admin/test-data",
|
|
156
|
+
headers=admin_headers,
|
|
157
|
+
json={"tag": "perf-test"}
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Parameterization Patterns
|
|
164
|
+
|
|
165
|
+
### Round-Robin (default for most tools)
|
|
166
|
+
Each VU/iteration picks the next row. Ensures even distribution.
|
|
167
|
+
|
|
168
|
+
### Random
|
|
169
|
+
Better for cache-busting tests where you want realistic cache miss rates.
|
|
170
|
+
|
|
171
|
+
### Unique-Per-VU
|
|
172
|
+
Each VU gets a dedicated row — critical for state-dependent flows (user owns specific order). Use VU index (`__VU` in k6, `${__threadNum}` in JMeter) to deterministically select a row.
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
// k6: VU-indexed data
|
|
176
|
+
const user = users[__VU - 1]; // VU 1 gets row 0, VU 2 gets row 1, etc.
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```xml
|
|
180
|
+
<!-- JMeter: use __threadNum to assign data -->
|
|
181
|
+
<!-- In CSV Data Set Config, Sharing Mode = Current Thread Group -->
|
|
182
|
+
<!-- Or use: ${__groovy(vars.get("__threadNum").toInteger() - 1)} -->
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Test Data Checklist
|
|
188
|
+
|
|
189
|
+
- [ ] Sufficient data volume (≥ peak VU count rows for unique-per-VU flows)
|
|
190
|
+
- [ ] Data is in the correct initial state for each scenario
|
|
191
|
+
- [ ] No hardcoded credentials or tokens
|
|
192
|
+
- [ ] Data isolated from production (separate tenant, tagging, or ephemeral env)
|
|
193
|
+
- [ ] Cleanup mechanism in place post-test
|
|
194
|
+
- [ ] CSV files available on all injector nodes (distributed tests)
|
|
195
|
+
- [ ] Sensitive data masked/anonymized (not using real PII in test data)
|
|
196
|
+
- [ ] Date/time sensitive records accounted for (e.g., expiry dates set far in future)
|