Guided Walkthrough

12 min read

This walkthrough builds the SecureBank performance test suite from an empty directory to a fully wired CI/CD pipeline. Each step produces a concrete artefact — a file, a configuration, a working test run. Work through them in order.

Build progression

Step 1 of 12

Step 1 — Project structure

Create the folder layout and properties files. Everything else hangs off this structure. Git-init the directory and add a .gitignore for results/ and any secrets files.

Step 1 — Project structure

mkdir securebank-perf && cd securebank-perf && git init
securebank-perf/
├── test-plans/
│   ├── smoke.jmx
│   ├── load.jmx
│   ├── stress.jmx
│   └── spike.jmx
├── environments/
│   ├── dev.properties
│   ├── staging.properties
│   └── production.properties.template
├── data/
│   ├── users.csv
│   └── accounts.csv
├── scripts/
│   ├── generate-data.py
│   └── check-thresholds.py
├── .gitignore
└── results/            (gitignored)

.gitignore:

results/
*.jtl
report*/
production.properties

environments/staging.properties:

baseUrl=https://api.staging.securebank.example
vusers=100
rampUpSecs=180
durationSecs=1800
thinkTimeMeanMs=3000
thinkTimeDeviationMs=1500

environments/dev.properties:

baseUrl=https://api.dev.securebank.example
vusers=1
rampUpSecs=1
durationSecs=60
thinkTimeMeanMs=500
thinkTimeDeviationMs=200

Step 2 — Login fragment with correlation

In load.jmx, create the login sequence inside a Once Only Controller:

Test Plan
└── Thread Group
    ├── User-Defined Variables (baseUrl, thinkTimeMeanMs, thinkTimeDeviationMs)
    ├── HTTP Request Defaults (server: parsed from ${baseUrl})
    ├── HTTP Cookie Manager
    ├── HTTP Header Manager (Content-Type: application/json, Accept: application/json)
    └── Once Only Controller
          ├── POST /auth/login
          │   Body: {"email":"${email}","password":"${password}"}
          │   ├── JSON Extractor: jwt ← $.access_token
          │   ├── JSON Extractor: csrfToken ← $.csrf_token
          │   └── Response Assertion: Response Code = 200
          └── HTTP Header Manager (Authorization: Bearer ${jwt}, X-CSRF-Token: ${csrfToken})
              (placed at Thread Group scope so all subsequent requests carry these headers)

Verify before continuing: add a Debug Sampler after the Once Only Controller. Run with 1 user, 1 loop. Click the Debug Sampler result in View Results Tree and confirm jwt and csrfToken contain real values, not NOT_FOUND. Remove the Debug Sampler when confirmed.

Step 3 — Module Controller pattern

  1. Right-click Test Plan → Add → Test Fragment
  2. Move the Once Only Controller (and its children) into the Test Fragment. Rename the Test Fragment "Login Fragment".
  3. Where the login used to be, add a Module Controller. Set its "Module To Run" to the Login Fragment.

Now every Thread Group that needs login adds one Module Controller referencing the same fragment. Change the login payload, extractor, or assertion once — all Thread Groups inherit the change.

Step 4 — Test data generation

Generate large CSV files before the test runs. The Python script uses the Faker library:

# scripts/generate-data.py
from faker import Faker
import csv, random, string
 
fake = Faker()
 
# users.csv
with open('data/users.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    for i in range(10000):
        mfa_key = ''.join(random.choices(string.digits, k=6))
        writer.writerow([
            f"perf.user.{i:05d}@securebank.test",
            "SecureBank_P4ss!",
            mfa_key
        ])
 
# accounts.csv  (5 accounts per user = 50,000 rows)
with open('data/accounts.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    for user_id in range(10000):
        for acct_type in ['CHECKING', 'SAVINGS', 'CREDIT', 'LOAN', 'INVESTMENT']:
            account_id = f"ACC-{user_id:05d}-{acct_type[:3]}"
            writer.writerow([account_id, user_id, acct_type])

Add two CSV Data Set Config elements to the Thread Group:

  • users.csv → variables: email,password,mfaKey — sharing: All threads
  • accounts.csv → variables: accountId,userId,accountType — sharing: All threads

Step 5 — Mixed scenario Thread Group structure

Test Plan
├── User-Defined Variables  (global config)
├── HTTP Request Defaults
├── HTTP Cookie Manager
├── HTTP Header Manager
├── Login Fragment (Test Fragment — not executed directly)
└── Thread Group (100 users, rampUp=${rampUpSecs}, duration=${durationSecs})
    ├── Module Controller → Login Fragment
    ├── Throughput Controller (60% — balance check)
    │   ├── GET /accounts/${accountId}/balance
    │   │   ├── JSON Assertion: $.balance exists
    │   │   └── Duration Assertion: 300ms
    │   └── Gaussian Random Timer (mean: ${thinkTimeMeanMs})
    ├── Throughput Controller (20% — transaction review)
    │   ├── GET /accounts/${accountId}/balance
    │   ├── GET /accounts/${accountId}/transactions?days=30
    │   │   ├── JSON Assertion: $.transactions exists
    │   │   └── Duration Assertion: 1500ms
    │   └── Gaussian Random Timer
    ├── Throughput Controller (15% — fund transfer)
    │   ├── GET /accounts/${accountId}/balance
    │   ├── POST /transfers
    │   │   Body: {"fromAccount":"${accountId}","amount":${__Random(10,500,)}}
    │   │   ├── JSON Extractor: transactionId ← $.transactionId
    │   │   └── Duration Assertion: 2000ms
    │   ├── Gaussian Random Timer (mean: 10000ms — user reviewing)
    │   ├── POST /transfers/${transactionId}/confirm
    │   └── Gaussian Random Timer
    └── Throughput Controller (5% — bill payment)
        ├── GET /accounts/${accountId}/balance
        ├── GET /payments/bills/${__Random(1,200,billId)}
        │   └── JSON Extractor: billReference ← $.reference
        ├── Gaussian Random Timer (mean: 12000ms — user reading bill)
        ├── POST /payments/bills
        │   Body: {"billReference":"${billReference}","amount":${__Random(50,500,)}}
        │   └── JSON Extractor: paymentId ← $.paymentId
        └── POST /payments/bills/${paymentId}/confirm

Steps 6–7 — Timers and assertions

Timers are already placed in the structure above. Confirm:

  • Thread Group-level timer covers all samplers
  • Higher-mean timers are placed before high-stakes actions (transfer, bill pay)

For assertions, the Duration Assertions are shown above. Add these Response Assertions to every sampler:

  • Field: Response Code, Rule: Matches, Pattern: 2\d{2}

This catches server errors without needing to know the exact success code for each endpoint.

Steps 8–10 — Backend Listener, stress, and spike

The Backend Listener configuration from Chapter 7 applies here without changes. Add it at Test Plan level so all Thread Groups write to the same InfluxDB series.

For stress.jmx: copy load.jmx, replace the Thread Group with a Stepping Thread Group. Configuration:

  • This group will start: 25
  • First wait for: 30 seconds
  • Then start: 25 threads
  • Every: 120 seconds
  • Keep adding threads, until maximum: 400

For spike.jmx: use Ultimate Thread Group. Three rows:

  1. 10 threads, ramp 30s, hold 300s, shutdown 30s, delay 0
  2. 300 threads, ramp 5s, hold 120s, shutdown 5s, delay 0
  3. 10 threads, ramp 30s, hold 300s, shutdown 0, delay 0

Steps 11–12 — Pipeline and dashboards

The GitHub Actions workflow from Chapter 7 runs here with smoke.jmx on PRs and load.jmx on the nightly schedule. The quality gate script checks p95 per sampler against the SLA table from the project brief.

For Grafana: import dashboard 5496 (as shown in the previous lesson). Add two custom panels on a new row:

Panel 1 — p95 per endpoint (stat panel):

FROM jmeter WHERE statut = success GROUP BY sampler_label SELECT percentile(resp_time, 95)

Panel 2 — Error rate over time:

FROM jmeter SELECT sum("errorCount") / sum("count") * 100 GROUP BY time(30s)

Run the load test. Watch all panels populate. If the p95 for any endpoint exceeds its threshold, the Grafana panel turns red (configure alert thresholds on each panel). The CI/CD quality gate catches the same breach in the pipeline.

Validating your deliverables

Before calling the capstone complete, verify each deliverable:

  • Project is in git with a clean .gitignore
  • All four test plans run without errors (smoke with 1 user first)
  • CSV extractors produce real values (verified via Debug Sampler)
  • Throughput Controller percentages sum to 100%
  • Duration Assertions match the SLA table in the brief
  • Backend Listener sends data to InfluxDB during a test run
  • Grafana dashboard shows live data during a test
  • HTML report generates without error after a CLI run
  • Quality gate script exits non-zero when a threshold is exceeded
  • CI/CD pipeline file is committed and triggered manually once successfully

// tip to track lessons you complete and pick up where you left off across devices.