Chapter 03Lesson 02~160 minutes

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.

Scope labHeader ManagerTimerAssertionPost-Processor

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

Hard ceiling: every experiment uses one thread, one loop, two HTTP requests, and 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
  1. HTTP Request Defaults: protocol http, server 127.0.0.1, port 8000.
  2. Header Manager: X-Lab-Scope = broad.
  3. Constant Timer: 150 ms.
  4. Response Assertion: Text Response contains "scope_header": "broad".
  5. Regular Expression Extractor: variable last_request_id; regex "request_id":\s*(\d+); template $1$; match 1; default NOT_FOUND.
  6. Alpha: GET /alpha, 1000 ms connect timeout, 2000 ms response timeout.
  7. Beta: GET /beta, same timeouts.
  8. Debug Sampler: show JMeter variables only.
  9. 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.log for execution comparisons.
  • Fixture /stats before/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?

Why is the 150 ms timer not expected directly in sampler elapsed?

After moving the extractor under Alpha, why does Debug Sampler still show Alpha ID after Beta?

Why disable Debug Sampler and View Results Tree in the load-evidence copy?

Why is this safer than a public demo target?

Next lesson

Design scope for reviewability

Lesson 3 converts the movement experiments into patterns for broad defaults, narrow overrides, controller depth, manager ownership, transaction labels, and lean load-result capture.

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 and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.