Test Plan Tree, Scope, Execution Order, and Component Semantics: Guided Hands-On Workflow
Make JMeter scope observable. A disposable loopback service echoes a synthetic request header and request ID so you can predict which samplers a Header Manager, Timer, Assertion, and Post-Processor affect before moving them, then verify with Debug Sampler, JTL, target counters, and engine logs.
Learning objectives
- Build a two-request scope lab with one clearly named controller.
- Use Header Manager scope to make target-observed request state visible.
- Use a Constant Timer and timestamps to separate pacing from response time.
- Move a Response Assertion and predict which samples become failures.
- Move a Regular Expression Extractor and observe the thread variable with Debug Sampler.
-
Preserve before/after JMX, JTL,
jmeter.log, and target counters.
1. Lab safety envelope
http://127.0.0.1:8000.
Debug Sampler is local-only and does not contact the target. Restart
the fixture between evidence runs. Do not add users/loops or
redirect the plan.
2. Start a target that makes scope visible
Save this as fixtures/scope_fixture.py. It returns the
request path, a synthetic request ID, and the incoming
X-Lab-Scope value.
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json, threading, time
HOST='127.0.0.1'; PORT=8000
lock=threading.Lock(); total=0; by_path={'/alpha':0,'/beta':0}
class Handler(BaseHTTPRequestHandler):
def send_json(self,status,payload):
body=json.dumps(payload,sort_keys=True).encode()
self.send_response(status); self.send_header('Content-Type','application/json'); self.send_header('Content-Length',str(len(body))); self.end_headers(); self.wfile.write(body)
def do_GET(self):
global total
if self.path=='/health': self.send_json(200,{'status':'ok'}); return
if self.path=='/stats':
with lock: payload={'total':total,'by_path':dict(by_path)}
self.send_json(200,payload); return
if self.path not in by_path: self.send_json(404,{'error':'not_found','path':self.path}); return
with lock:
total+=1; by_path[self.path]+=1; request_id=total
time.sleep(0.025)
self.send_json(200,{'status':'ok','path':self.path,'request_id':request_id,'scope_header':self.headers.get('X-Lab-Scope','<missing>')})
def log_message(self,format,*args): return
if __name__=='__main__':
print(f'fixture=http://{HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT),Handler).serve_forever()
Start and preflight:
python fixtures/scope_fixture.py
# second terminal
curl --fail --silent http://127.0.0.1:8000/health
curl --fail --silent http://127.0.0.1:8000/stats
PowerShell:
(Invoke-WebRequest -UseBasicParsing http://127.0.0.1:8000/health).Content
(Invoke-WebRequest -UseBasicParsing http://127.0.0.1:8000/stats).Content
Fresh state should report total=0.
3. Build the broad-scope baseline tree
In the JMeter GUI, build and save
plans/scope-broad.jmx:
Test Plan
└── Thread Group — 1 thread, 1 loop
├── HTTP Request Defaults — http / 127.0.0.1 / 8000
├── Simple Controller — Scope Lab
│ ├── HTTP Header Manager — X-Lab-Scope: broad
│ ├── Constant Timer — 150 ms
│ ├── Response Assertion — body contains "scope_header": "broad"
│ ├── Regular Expression Extractor
│ │ └── last_request_id <- "request_id":\s*(\d+)
│ ├── HTTP Request — Alpha — GET /alpha
│ └── HTTP Request — Beta — GET /beta
├── Debug Sampler — Inspect variables after Scope Lab
└── View Results Tree — AUTHORING DEBUG ONLY
-
HTTP Request Defaults: protocol
http, server127.0.0.1, port8000. -
Header Manager:
X-Lab-Scope = broad. - Constant Timer: 150 ms.
-
Response Assertion: Text Response contains
"scope_header": "broad". -
Regular Expression Extractor: variable
last_request_id; regex"request_id":\s*(\d+); template$1$; match 1; defaultNOT_FOUND. -
Alpha: GET
/alpha, 1000 ms connect timeout, 2000 ms response timeout. -
Beta: GET
/beta, same timeouts. - Debug Sampler: show JMeter variables only.
- View Results Tree: authoring debug only.
4. Predict baseline behavior
| Observation | Prediction | Why |
|---|---|---|
| Alpha header | broad | Header Manager is in Alpha ancestor scope. |
| Beta header | broad | Same controller scope includes Beta. |
| Assertions | Both pass | Both responses echo broad. |
| Extractor | Runs after Alpha and Beta | It is broad to both network samplers. |
| Debug variable | Beta request ID | Beta is the last network sampler that refreshes last_request_id. |
| Timer | 150 ms before Alpha and Beta | Timer is broad to both network samplers. |
Debug Sampler is outside Scope Lab, so controller-level Header Manager, Timer, Assertion, and Extractor do not apply to the Debug Sampler itself.
5. Run one bounded GUI diagnostic
Run once in GUI mode because this is functional tree debugging, not
load. In View Results Tree, confirm Alpha/Beta response bodies,
assertion outcomes, and Debug Sampler's
last_request_id. Compare sampler start times; do not
expect the 150 ms timer to become 150 ms of server
elapsed.
Restart the fixture afterward so CLI evidence begins from a known target count.
6. Save a lean load-evidence copy and run CLI
Save scope-broad-load.jmx with View Results Tree and
Debug Sampler disabled. Keep Alpha/Beta and the scoped elements
unchanged.
mkdir -p results/run-001
jmeter -n -t plans/scope-broad-load.jmx \
-l results/run-001/results.jtl \
-j results/run-001/jmeter.log
New-Item -ItemType Directory -Force results\run-001 | Out-Null
jmeter.bat -n `
-t plans\scope-broad-load.jmx `
-l results\run-001\results.jtl `
-j results\run-001\jmeter.log
A fresh fixture should report two target calls: Alpha 1 and Beta 1.
7. Inspect labels and start-time gaps
Confirm the CSV JTL includes label,
success, elapsed, and
timeStamp, then use:
import csv
from pathlib import Path
path=Path('results/run-001/results.jtl')
rows=list(csv.DictReader(path.open(encoding='utf-8')))
print(f'samples={len(rows)}')
for r in rows:
print(f"{r['label']}: success={r['success']} elapsed_ms={r['elapsed']} timestamp={r['timeStamp']}")
network=[r for r in rows if r['label'] in {'Alpha','Beta'}]
if len(network)==2:
print('alpha_to_beta_start_gap_ms='+str(int(network[1]['timeStamp'])-int(network[0]['timeStamp'])))
Alpha-to-Beta start gap includes Alpha completion plus the timer before Beta. Exact values vary. The causal rule matters more than a particular millisecond value.
8. Experiment A: narrow the Header Manager to Alpha
Copy the broad JMX. Move only Header Manager under Alpha; leave the Response Assertion broad:
Test Plan
└── Thread Group
├── HTTP Request Defaults
├── Simple Controller — Scope Lab
│ ├── Constant Timer — 150 ms
│ ├── Response Assertion — expects scope_header=broad
│ ├── Regular Expression Extractor — last_request_id
│ ├── HTTP Request — Alpha
│ │ └── HTTP Header Manager — X-Lab-Scope: broad
│ └── HTTP Request — Beta
├── Debug Sampler
└── View Results Tree
Predict: Alpha still echoes broad and passes; Beta echoes
<missing>; Beta fails because the broad assertion
still expects broad; the extractor remains broad so Debug Sampler
still ends with Beta's request ID.
Run one bounded debug pass and preserve the failure. It proves request configuration changed because scope changed.
9. Experiment B: narrow the timer to Alpha
Restore broad Header Manager. Move only Constant Timer under Alpha. Predict Alpha gets the 150 ms pre-sampler wait while Beta does not. Compare JTL start timestamps in a new run. Sampler elapsed should still mostly reflect the local fixture's ~25 ms service delay plus client overhead.
10. Experiment C: narrow the assertion to Alpha
Start from the header-narrow tree and move Response Assertion under
Alpha too. Beta still returns <missing>, but it
no longer runs that assertion. A successful Beta therefore means
“HTTP request succeeded under its current checks,” not “the
broad-header rule passed.”
11. Experiment D: narrow the extractor to Alpha
Restore headers/assertions to broad. Move only Regular Expression
Extractor under Alpha. Alpha sets last_request_id; Beta
runs without refreshing it. The Debug Sampler after Scope Lab should
still show Alpha's ID. Thread variables persist until another
element changes them.
12. Preserve before/after evidence
- JMX before and after each move.
- Tree screenshot/text export.
- Prediction written before execution.
- Bounded Debug Sampler/View Results Tree observation when needed.
-
CLI JTL and
jmeter.logfor execution comparisons. - Fixture
/statsbefore/after. - One-sentence conclusion naming changed scope and observed effect.
13. Challenge: where should a Beta-only correctness rule live?
You need Beta to require "path": "/beta", but Alpha
must not run that assertion. Place the Response Assertion directly
under Beta. Explain why putting it under Scope Lab would broaden it
to Alpha.
Knowledge check
After moving Header Manager under Alpha, why does Beta fail while the assertion remains broad?
Beta no longer receives the header, but the controller-level assertion still applies and expects broad in Beta response.
Why is the 150 ms timer not expected directly in sampler elapsed?
The timer runs before the sampler; observe timestamps/iteration duration/throughput rather than treating it as server response time.
After moving the extractor under Alpha, why does Debug Sampler still show Alpha ID after Beta?
Beta no longer triggers the extractor, so nothing overwrites the thread-local value created by Alpha.
Why disable Debug Sampler and View Results Tree in the load-evidence copy?
They are diagnostic elements whose synthetic samples and detailed rendering can add overhead or contaminate the result population.
Why is this safer than a public demo target?
The loopback fixture is controlled, synthetic, observable, bounded to two requests, and restartable to known state.
Official references and version notes
- Elements of a Test Plan — execution order, scoping rules, variables/properties, timers, assertions, configuration elements, processors, listeners, controllers, and samplers.
- Component Reference — Header Manager, Constant Timer, Response Assertion, Regular Expression Extractor, Debug Sampler, Transaction Controller, and listeners.
- Functions and Variables — sampler-context functions and variable semantics.
- Hints and Tips — current quick-add bindings for common debug elements.
- Getting Started and Best Practices — GUI authoring versus CLI load execution and lean result collection.
- Apache JMeter downloads — current production release and Java requirement.
Version-sensitive statements were rechecked against current Apache
JMeter primary documentation on 2026-09-04. The baseline is Apache
JMeter 5.6.3 with a Java 17 JDK for labs and no
third-party plugins; JMeter 5.6.3 itself requires Java 8+. The
current Test Plan manual defines applicable execution order as
Configuration Elements → Pre-Processors → Timers → Sampler →
Post-Processors → Assertions → Listeners. Controllers and samplers
are primarily ordered; listeners, configuration elements,
pre/post-processors, assertions, and timers are
hierarchical/scoped. Mandatory traffic is restricted to
http://127.0.0.1:8000, one thread and one loop. Debug
Sampler/View Results Tree are bounded GUI diagnostics only; CLI
remains the load-evidence path.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.