Skip to content
SDETRole
SDETRole

Lesson 2 of 9 · Advanced Quality Engineering

JMeter Basics

Build and run load tests with Apache JMeter 5.6: thread groups, samplers, assertions, timers, CSV data, correlation with extractors and non-GUI runs with HTML reports.

intermediate 9 min read Updated

You will learn to

  • Build a JMeter test plan from thread groups, samplers, config elements, timers and assertions
  • Parameterise users with CSV Data Set Config and correlate dynamic values with extractors
  • Run load tests in non-GUI mode and generate the HTML dashboard report
  • Avoid the common mistakes that make JMeter results meaningless

Prerequisites

  • Performance Testing Fundamentals
  • HTTP Fundamentals
  • Basic command line

Apache JMeter is an open-source, Java-based load testing tool. It has been around for a long time, it is free, it speaks HTTP and many other protocols, and a large number of performance testing jobs still list it. Newer code-first tools such as k6, Gatling and Locust are popular too, but the concepts you learn in JMeter (virtual users, ramp-up, think time, correlation, percentiles) carry over to all of them.

This lesson uses JMeter 5.6. Install it by downloading the binary archive from the Apache JMeter site and running bin/jmeter (or bin\jmeter.bat on Windows); it needs a Java runtime, and a current LTS such as Java 17 or 21 is a safe choice.

How a test plan is organised

A JMeter test plan is a tree of elements saved as an XML file with a .jmx extension. You build it in the GUI, then run it from the command line. The main element types:

Element What it does Examples
Thread Group Defines the virtual users and how they ramp up Thread Group
Sampler Sends a request and records the result HTTP Request, JDBC Request
Config Element Shared settings and test data HTTP Request Defaults, HTTP Cookie Manager, HTTP Header Manager, CSV Data Set Config
Timer Adds delay (think time) before samplers Constant Timer, Uniform Random Timer, Constant Throughput Timer
Assertion Checks each response Response Assertion, JSON Assertion, Duration Assertion
Post-Processor Extracts values from responses JSON Extractor, Regular Expression Extractor, Boundary Extractor
Logic Controller Controls flow Transaction Controller, If Controller, Loop Controller
Listener Shows or saves results View Results Tree, Summary Report, Simple Data Writer

Scope matters. Config elements, timers, assertions and post-processors apply to the samplers at their level of the tree and below. An assertion placed directly under the Thread Group checks every sampler in it; placed under one HTTP Request, it checks only that request. Many confusing results come from elements placed at the wrong level.

A first plan: browse and log in

A realistic structure for an API login-and-browse journey:

Test Plan
└── Thread Group  (users, ramp-up, duration)
    ├── HTTP Request Defaults   (protocol https, server api.staging.example.com)
    ├── HTTP Header Manager     (Content-Type: application/json)
    ├── HTTP Cookie Manager
    ├── CSV Data Set Config     (users.csv -> username, password)
    ├── Transaction Controller: Login
    │   └── HTTP Request: POST /auth/login
    │       ├── JSON Extractor  (token from $.accessToken)
    │       └── Response Assertion (status 200)
    ├── Uniform Random Timer    (think time)
    └── Transaction Controller: Browse
        ├── HTTP Request: GET /products?page=1
        │   └── JSON Assertion  ($.items exists)
        └── HTTP Request: GET /products/101

Thread Group

The Thread Group defines load:

  • Number of Threads (users): concurrent virtual users. Each thread runs the plan’s children in order, like a user following a script.
  • Ramp-up period: seconds to start all threads. 100 threads with a 100-second ramp-up starts roughly one per second. Ramping up gradually lets you see where response times start to degrade, and avoids an unrealistic thundering herd.
  • Loop Count or Duration: how long each thread repeats. For load tests, use a duration (tick “Specify Thread lifetime”) so the test runs for a fixed time, for example 15 minutes at steady state.

Do not hard-code these numbers. Use properties so the same plan can run a smoke check with 2 users and a load test with 200:

Number of Threads:  ${__P(threads,10)}
Ramp-up period:     ${__P(rampup,60)}
Duration (seconds): ${__P(duration,600)}

__P reads a property passed on the command line with -J, falling back to the default after the comma.

Samplers and defaults

Put the protocol, host and port in HTTP Request Defaults once, so each HTTP Request only needs a method and path. That makes switching environments a single change (or another property: ${__P(host,api.staging.example.com)}).

Add an HTTP Cookie Manager if the application uses cookies, so each thread keeps its own session like a real browser. Note that HTTP Request samplers fetch only what you ask for; they do not run JavaScript or render pages, and they only download embedded resources if you enable that option.

Assertions

Without assertions, JMeter counts any response it receives, including a fast “500 Internal Server Error” or a login page served instead of data, as a success. Fast errors make response times look great.

  • Response Assertion: check the response code, or that the body contains or does not contain text.
  • JSON Assertion: check that a JSON path exists or has a value.
  • Duration Assertion: mark a sample failed if it took longer than a threshold.

Keep assertions light. Complex assertions on every response cost CPU on the load generator, which can distort results at high load.

Timers: think time

Real users pause between actions. Without timers, each thread fires requests back to back, so 50 threads generate far more load than 50 real users would. A Uniform Random Timer (for example 2 to 5 seconds) is a reasonable default. Timers run before each sampler in their scope, so a timer at Thread Group level delays every request.

If your goal is a specific request rate rather than a number of users, the Constant Throughput Timer paces samples to a target throughput per minute, as long as there are enough threads to achieve it.

Test data with CSV Data Set Config

Hundreds of threads logging in as the same user is unrealistic (caching, locking and session behaviour all differ) and can trip security controls. Feed distinct users from a file:

username,password
loadtest-user-001,Pa55word-001
loadtest-user-002,Pa55word-002
loadtest-user-003,Pa55word-003

In CSV Data Set Config set the filename, set Variable Names to username,password (or leave it empty and use the header line), and choose sharing and recycling behaviour. Reference the values as ${username} and ${password} in the request body:

{ "username": "${username}", "password": "${password}" }

Each thread takes the next line on each iteration by default. Set “Recycle on EOF” to false and “Stop thread on EOF” to true if each row must be used only once.

Use dedicated load-test accounts in a test environment. A test data generator can help produce the CSV.

Correlation with extractors

Most applications return dynamic values that must be sent back in later requests: session tokens, CSRF tokens, order IDs. Replaying a recorded value fails because the server issued a new one. Capturing and reusing these values is called correlation.

Add a JSON Extractor as a child of the login request:

  • Names of created variables: token
  • JSON Path expressions: $.accessToken
  • Default Values: TOKEN_NOT_FOUND

Then send it in later requests through an HTTP Header Manager inside the Browse controller:

Authorization: Bearer ${token}

For HTML or text responses use a Regular Expression Extractor or the simpler Boundary Extractor (left and right boundaries). Setting a default such as TOKEN_NOT_FOUND makes failures obvious: if you see it in a request, the extraction failed. Add an assertion so those requests are counted as errors rather than silently sending a bad token. You can try expressions first in a JSONPath tester or regex tester.

Listeners and running under load

Listeners are useful while building a plan. View Results Tree shows each request and response, which is ideal for debugging correlation with one or two threads.

Run the test from the command line:

jmeter -n -t plan.jmx -l results.jtl -e -o report \
  -Jthreads=200 -Jrampup=300 -Jduration=1800 -Jhost=api.staging.example.com
  • -n non-GUI mode, -t the test plan, -l the results file (JTL, CSV by default).
  • -e -o report generates the HTML dashboard report into the report folder at the end. The folder must be empty or not exist.
  • -J sets the properties read by __P.

You can also generate the dashboard later from an existing results file with jmeter -g results.jtl -o report.

Reading the results

The HTML dashboard includes an APDEX table, a statistics table per sampler and transaction (samples, error percentage, average, percentiles, throughput) and charts over time. Focus on:

  • Error percentage first. Response times for failed requests are meaningless.
  • Percentiles, not averages. The 95th and 99th percentiles show what slow users experience. An average can look fine while a significant share of users wait several seconds.
  • Throughput versus users. If throughput stops rising as users increase while response times climb, you have found a bottleneck.
  • Over time charts. Response time creeping up during a steady-state period can indicate resource exhaustion, such as connection pools or memory.

Also watch the load generator itself. If its CPU is saturated, JMeter cannot send requests on time and the results reflect the generator’s limits, not the system’s. For high loads, JMeter supports distributed testing across several generator machines.

Practice exercise

A colleague’s JMeter test shows an average response time of 120 ms and “great performance” with 300 threads. You notice: the plan has no assertions or timers, all threads log in as admin, the login token is hard-coded from a recording, View Results Tree is enabled, and it was run from the GUI. List the problems and how you would fix each.

Show a model answer
  1. No assertions: errors (401s from the stale token, 500s) are counted as successes, and fast errors pull the average down. Add Response or JSON Assertions on status and key content.
  2. Hard-coded token: every request after login is probably unauthorised. Add a JSON Extractor on the login response with a default like TOKEN_NOT_FOUND, and send Bearer ${token} via a Header Manager.
  3. Single admin user: unrealistic caching and locking, and a privileged account in a load test. Use CSV Data Set Config with dedicated test users.
  4. No timers: 300 threads without think time is far more load than 300 users. Add a Uniform Random Timer, or decide the target is throughput and use a Constant Throughput Timer.
  5. GUI and View Results Tree under load: the generator is overloaded and results are distorted. Run jmeter -n -t plan.jmx -l results.jtl -e -o report with GUI listeners disabled.
  6. Averages only: report error rate and 95th/99th percentiles per transaction from the HTML dashboard, with a gradual ramp-up and a steady-state duration.

Key takeaways

  • A JMeter plan is a tree; element scope determines which samplers timers, assertions and extractors apply to.
  • Thread Groups define users, ramp-up and duration; parameterise them with __P and -J.
  • Always add assertions, think time and realistic per-user data from CSV Data Set Config.
  • Correlate dynamic values with JSON, Regular Expression or Boundary Extractors, with visible defaults.
  • Debug in the GUI with a few threads; run load in non-GUI mode with jmeter -n -t plan.jmx -l results.jtl -e -o report.
  • Judge results by error rate and percentiles, and check the load generator is not the bottleneck.

Key terms

Finished this lesson?

Progress is saved in this browser. No account needed.

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →