Chapter 10: Practice: Load Test a Web App
Time to build. We are going to create a complete, production-quality JMeter test plan for the Shopping Portal checkout flow. This is not a toy example -- this is the exact structure you would use on a real project. I will walk through every single component and explain why it is there.
Think of a JMeter test plan like a well-organized kitchen. Everything has its place. Variables go at the top (like your spice rack -- accessible from anywhere). Thread Groups define the load (like your cooking stations). Inside each thread group, Transaction Controllers group related requests (like courses in a meal). And listeners at the bottom collect the results (like the waiters bringing feedback from the dining room).
Here is the full test plan, top to bottom. Study this like a blueprint -- every element exists for a reason.
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan"
testname="Shopping Portal Load Test">
<elementProp name="TestPlan.user_defined_variables"
elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="BASE_URL" elementType="Argument">
<stringProp name="Argument.value">localhost</stringProp>
</elementProp>
<elementProp name="PORT" elementType="Argument">
<stringProp name="Argument.value">3000</stringProp>
</elementProp>
<elementProp name="PROTOCOL" elementType="Argument">
<stringProp name="Argument.value">http</stringProp>
</elementProp>
<elementProp name="THINK_TIME_MIN" elementType="Argument">
<stringProp name="Argument.value">2000</stringProp>
</elementProp>
<elementProp name="THINK_TIME_MAX" elementType="Argument">
<stringProp name="Argument.value">4000</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</TestPlan>
<hashTree>
<!-- ==================== THREAD GROUP ==================== -->
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup"
testname="Shopping Flow Users">
<intProp name="ThreadGroup.num_threads">100</intProp>
<intProp name="ThreadGroup.ramp_time">60</intProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.duration">600</stringProp>
<elementProp name="ThreadGroup.main_controller"
elementType="LoopController">
<intProp name="LoopController.loops">5</intProp>
</elementProp>
</ThreadGroup>
<hashTree>
<!-- HTTP Request Defaults -->
<ConfigTestElement guiclass="HttpDefaultsGui"
testclass="ConfigTestElement"
testname="HTTP Request Defaults">
<stringProp name="HTTPSampler.domain">${BASE_URL}</stringProp>
<stringProp name="HTTPSampler.port">${PORT}</stringProp>
<stringProp name="HTTPSampler.protocol">${PROTOCOL}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
</ConfigTestElement>
<hashTree/>
<!-- HTTP Cookie Manager (session handling) -->
<CookieManager guiclass="CookiePanel"
testclass="CookieManager"
testname="HTTP Cookie Manager">
<boolProp name="CookieManager.clearEachIteration">
false
</boolProp>
</CookieManager>
<hashTree/>
<!-- HTTP Header Manager -->
<HeaderManager guiclass="HeaderPanel"
testclass="HeaderManager"
testname="HTTP Header Manager">
<collectionProp name="HeaderManager.headers">
<elementProp name="Content-Type" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">
application/json
</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">
application/json
</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<!-- CSV Data Set: Users -->
<CSVDataSet guiclass="TestBeanGUI"
testclass="CSVDataSet"
testname="CSV Users">
<stringProp name="filename">test-data/users.csv</stringProp>
<stringProp name="variableNames">
username,password
</stringProp>
<stringProp name="delimiter">,</stringProp>
<boolProp name="quotedData">false</boolProp>
<stringProp name="recycle">true</stringProp>
<stringProp name="stopThread">false</stringProp>
<stringProp name="shareMode">shareMode.all</stringProp>
</CSVDataSet>
<hashTree/>
<!-- CSV Data Set: Search Terms -->
<CSVDataSet guiclass="TestBeanGUI"
testclass="CSVDataSet"
testname="CSV Search Terms">
<stringProp name="filename">
test-data/search-terms.csv
</stringProp>
<stringProp name="variableNames">searchTerm</stringProp>
<stringProp name="delimiter">,</stringProp>
<boolProp name="quotedData">false</boolProp>
<stringProp name="recycle">true</stringProp>
<stringProp name="stopThread">false</stringProp>
<stringProp name="shareMode">shareMode.all</stringProp>
</CSVDataSet>
<hashTree/>
<!-- ======= TRANSACTION 01: Homepage ======= -->
<TransactionController guiclass="TransactionControllerGui"
testclass="TransactionController"
testname="01_Homepage">
<boolProp name="TransactionController.includeTimers">
false
</boolProp>
</TransactionController>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui"
testclass="HTTPSamplerProxy"
testname="GET Homepage">
<stringProp name="HTTPSampler.path">/</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<hashTree>
<ResponseAssertion guiclass="AssertionGui"
testclass="ResponseAssertion"
testname="Assert 200">
<intProp name="Assertion.test_type">2</intProp>
<stringProp name="Assertion.test_field">
Assertion.response_code
</stringProp>
<collectionProp name="Asserion.test_strings">
<stringProp>200</stringProp>
</collectionProp>
</ResponseAssertion>
<hashTree/>
<DurationAssertion guiclass="DurationAssertionGui"
testclass="DurationAssertion"
testname="Duration < 3s">
<longProp name="DurationAssertion.duration">
3000
</longProp>
</DurationAssertion>
<hashTree/>
</hashTree>
</hashTree>
<!-- Think Time after Homepage -->
<GaussianRandomTimer guiclass="GaussianRandomTimerGui"
testclass="GaussianRandomTimer"
testname="Think Time">
<stringProp name="ConstantTimer.delay">
${THINK_TIME_MIN}
</stringProp>
<stringProp name="RandomTimer.range">
${__intSum(${THINK_TIME_MAX},-${THINK_TIME_MIN})}
</stringProp>
</GaussianRandomTimer>
<hashTree/>
<!-- ======= TRANSACTION 02: Login ======= -->
<TransactionController guiclass="TransactionControllerGui"
testclass="TransactionController"
testname="02_Login">
<boolProp name="TransactionController.includeTimers">
false
</boolProp>
</TransactionController>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui"
testclass="HTTPSamplerProxy"
testname="POST Login">
<stringProp name="HTTPSampler.path">
/api/auth/login
</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments"
elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp elementType="HTTPArgument">
<stringProp name="Argument.value">
{"username":"${username}","password":"${password}"}
</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
<hashTree>
<ResponseAssertion guiclass="AssertionGui"
testclass="ResponseAssertion"
testname="Assert 200">
<intProp name="Assertion.test_type">2</intProp>
<stringProp name="Assertion.test_field">
Assertion.response_code
</stringProp>
<collectionProp name="Asserion.test_strings">
<stringProp>200</stringProp>
</collectionProp>
</ResponseAssertion>
<hashTree/>
<!-- Extract auth token from login response -->
<JSONPostProcessor guiclass="JSONPostProcessorGui"
testclass="JSONPostProcessor"
testname="Extract Auth Token">
<stringProp name="JSONPostProcessor.referenceNames">
authToken
</stringProp>
<stringProp name="JSONPostProcessor.jsonPathExprs">
$.token
</stringProp>
<stringProp name="JSONPostProcessor.match_numbers">
1
</stringProp>
</JSONPostProcessor>
<hashTree/>
</hashTree>
</hashTree>
<!-- Think Time -->
<GaussianRandomTimer guiclass="GaussianRandomTimerGui"
testclass="GaussianRandomTimer"
testname="Think Time">
<stringProp name="ConstantTimer.delay">
${THINK_TIME_MIN}
</stringProp>
<stringProp name="RandomTimer.range">
${__intSum(${THINK_TIME_MAX},-${THINK_TIME_MIN})}
</stringProp>
</GaussianRandomTimer>
<hashTree/>
<!-- Remaining transactions follow same pattern -->
<!-- 03_Search, 04_ProductDetail, 05_AddToCart, -->
<!-- 06_ViewCart, 07_Checkout, 08_OrderConfirmation -->
<!-- (See full breakdown in the next section) -->
</hashTree>
<!-- Listeners -->
<ResultCollector guiclass="StatVisualizer"
testclass="ResultCollector"
testname="Aggregate Report">
<stringProp name="filename">results/aggregate.csv</stringProp>
</ResultCollector>
<hashTree/>
<ResultCollector guiclass="SummaryReport"
testclass="ResultCollector"
testname="Summary Report">
<stringProp name="filename">results/summary.csv</stringProp>
</ResultCollector>
<hashTree/>
</hashTree>
</hashTree>
</jmeterTestPlan>| Element | Purpose | What Happens Without It |
|---|---|---|
| User Defined Variables | Centralize config so you change values in one place | Hardcoded values scattered everywhere, maintenance nightmare |
| HTTP Request Defaults | Set server, port, protocol once for all requests | You type the same server name in every single request |
| HTTP Cookie Manager | Automatically handle session cookies across requests | Login works but all subsequent requests lose the session |
| HTTP Header Manager | Set Content-Type and Accept headers globally | API requests fail because server expects JSON but gets form data |
| CSV Data Set (Users) | Feed different credentials to each virtual user | All 100 users log in as the same user -- unrealistic |
| CSV Data Set (Search Terms) | Different users search for different products | Server caches the first search result, inflating performance numbers |
| Transaction Controllers | Group related requests and measure them as one unit | You see individual request times but not "how long did checkout take?" |
| Gaussian Random Timer | Simulate realistic user think time between actions | Users fire requests with zero delay -- unrealistic machine-gun traffic |
| Response Assertions | Verify responses are correct, not just fast | A 200 OK with an empty body or error message counts as "passed" |
| Duration Assertions | Fail requests that exceed SLA thresholds | Slow responses slip through and you only catch them in analysis |
| JSON Extractor | Capture dynamic values for correlation | Subsequent requests use stale/missing IDs and fail |
Save this test plan structure as a template. Every e-commerce performance test you ever do will follow roughly this same pattern. Change the URLs, the payloads, and the CSV data -- but the architecture stays the same.
Key Point: A production-quality test plan has five layers: configuration (variables, defaults), data feeds (CSV files), request logic (transaction controllers with assertions), correlation (extractors), and timing (think times). Skip any layer and your results are unreliable.
Key Point: A complete JMeter test plan needs variables, HTTP defaults, cookie management, CSV data feeds, transaction controllers, assertions, extractors, think times, and listeners.