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.
@@ -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)