REST, JSON, GraphQL & SOAP APIs
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. |
The reference material consulted while preparing these pages is listed in the section bibliography.
This page was written against Apache JMeter 5.6.3 (latest release on 2026-10-14; it runs on Java 8 and later, Java 17
or later is recommended), with the differences planned for JMeter 6.0 noted where they matter. JMeter 6.0 is not
released: the planned changes come from xdocs/changes.xml on the master branch, need Java 17 or later (Java 21
recommended) and are described as planned, not as shipped behaviour. Everything on this page is core JMeter, so no
plugin is needed. The JSON elements evaluate paths with the Jayway JsonPath library (the component reference documents
the syntax of JsonPath 1.2.0; 6.0 plans to move to json-path 3.0.0), the XPath2 elements run on Saxon-HE (6.0 plans
Saxon-HE 13), and the GraphQL HTTP Request exists since JMeter 5.4. The books on the market target JMeter 2.11 to 3.0:
they test SOAP with a dedicated sampler that no longer exists and parse JSON with a jmeter-plugins post-processor that
core JMeter has since replaced. Every field name, property and rule below is taken from the official component
reference, functions reference, user manual and change log, and where the documentation is silent, from the 5.6.3
source code, which is said explicitly.
The HTTP mechanics (the HTTP Request sampler, HTTP Request Defaults, implementations, timeouts, HTTPS) are covered in HTTP Requests, and the managers that carry state between requests (headers, cookies, tokens) in HTTP Sessions, Headers, Cookies & Authentication. This page builds on both and concentrates on what changes when the system under test is an API rather than a web page: bodies in JSON or XML, the right content type, every HTTP method, status codes as part of the contract, and response checks that understand the payload. The server side of the Bookshelf REST API is the kind of service built in REST APIs with Spring MVC and WebFlux, and its GraphQL endpoint the kind described in the GraphQL Reference; this page does not repeat how to build them.
Where API testing sits in a test plan
An API test in JMeter is an ordinary HTTP test with a few elements in specific places. The table maps each need to the core element that covers it.
| Need | Element (menu path) | Where it goes |
|---|---|---|
Host, port, protocol shared by all requests |
HTTP Request Defaults (Add → Config Element → HTTP Request Defaults) |
Thread Group level, filled from |
|
HTTP Header Manager (Add → Config Element → HTTP Header Manager) |
Thread Group level for headers every request needs; under a sampler for headers only that request needs. |
A request with a JSON or XML body |
HTTP Request (Add → Sampler → HTTP Request), Body Data tab |
Inside the Thread Group or a controller, one per API operation. |
A GraphQL operation |
GraphQL HTTP Request (Add → Sampler → GraphQL HTTP Request) |
Same place as any sampler. |
Read a value (an id, a token) from a JSON response |
JSON Extractor (Add → Post Processors → JSON Extractor) |
As a child of the sampler whose response holds the value. |
Read a value from an XML or SOAP response |
XPath2 Extractor (Add → Post Processors → XPath2 Extractor) |
As a child of the sampler. |
Check the status code |
Response Assertion (Add → Assertions → Response Assertion), Field to Test = Response Code |
As a child of the sampler. |
Check the JSON payload |
JSON Assertion (Add → Assertions → JSON Assertion) |
As a child of the sampler. |
Check the XML payload, or a SOAP fault |
XPath2 Assertion (Add → Assertions → XPath2 Assertion) |
As a child of the sampler. |
The execution order for one sampler is the one described in Test Plan Elements, Execution Order & Scoping: configuration elements (Defaults, Header Manager), pre-processors, timers, the sampler, post-processors (the extractors), assertions, then listeners. Extractors therefore run before assertions on the same response, so a JSON Extractor sets its variable even when a later assertion fails the sample; a default value that makes the failure visible matters for that reason.
REST requests with JSON bodies
Body Data versus Parameters
The HTTP Request sampler has three tabs below the path. For APIs the choice between them decides how the request body is built:
| Tab | What JMeter sends | Use it for |
|---|---|---|
Parameters |
For GET and DELETE, a query string appended to the URL. For POST and PUT, a form-encoded body (or a multipart body when Use multipart/form-data is ticked). Each row has the columns Name, Value, URL Encode?, Content-Type and Include Equals?. |
Query strings on GET and DELETE; HTML-form style POSTs. |
Body Data |
The text of the tab, exactly as typed after variable and function substitution, as the request body. Each line is sent with CRLF appended, except the last one. |
JSON, XML and SOAP bodies on POST, PUT and PATCH (and on GET or DELETE if the API really expects a body). |
Files Upload |
A file. A single file with an empty Parameter Name on POST, PUT or PATCH is sent as the whole body, with its MIME Type as the content type; otherwise the request becomes multipart. |
Large or binary bodies kept outside the |
The component reference lists JSON REST, XML REST and SOAP requests among the uses of the Body Data tab. Once a
sampler has Body Data, the GUI does not let you go back to the Parameters tab until the Body Data is cleared. Under
the hood Body Data is a single parameter with no name, and the JMX records it with HTTPSampler.postBodyRaw set to
true.
Two body-related fields of the sampler matter for APIs:
| Field | Meaning | JMX property |
|---|---|---|
Method |
GET, POST, HEAD, TRACE, OPTIONS, PUT, DELETE, PATCH (PATCH is not supported by the Java implementation). With
HttpClient4 the WebDAV methods are allowed too, and more can be declared with the property
|
|
Content encoding |
The character encoding of the body for POST, PUT, PATCH and FILE. It is the charset JMeter uses to turn the text
into bytes, not the |
|
JSON bodies with variables
A JSON body is typed into Body Data with variable references where the values change per user or per iteration.
JMeter substitutes ${...} references and functions before sending; it does not parse or validate the JSON. The
loan request of the Bookshelf journey, for a book id picked from the search results and a due date three weeks ahead:
{
"bookId": "${bookId}",
"dueDate": "${__timeShift(yyyy-MM-dd,,P21D,,)}",
"channel": "load-test"
}
Steps in the GUI:
-
Select the sampler and set Method to
POSTand Path to/api/loans. -
Open the Body Data tab and paste the JSON.
-
Make sure an HTTP Header Manager in scope sends
Content-Type: application/json(next section). -
Run the plan once with a View Results Tree listener enabled and check the Request tab: the body must show the substituted values, not the literal
${bookId}.
The resulting .jmx fragment:
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="POST /api/loans">
<stringProp name="HTTPSampler.path">/api/loans</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{"bookId": "${bookId}", "dueDate": "${__timeShift(yyyy-MM-dd,,P21D,,)}", "channel": "load-test"}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
</HTTPSamplerProxy>
<hashTree/>
Three rules keep JSON bodies correct under load:
-
Quote string variables in the template, not in the data.
"bookId": "${bookId}"with a bare id in the CSV file keeps the data file simple and the JSON valid. Numbers and booleans go without quotes:"copies": ${copies}. -
Values must already be JSON-safe. JMeter inserts the text as is. A search term or a name with a double quote or a backslash breaks the body. Core JMeter has
__escapeXmland__urlencodebut no JSON-escaping function; clean the test data up front, or let Groovy produce a quoted JSON string with${__groovy(groovy.json.JsonOutput.toJson(vars.get("term")))}(the result includes the surrounding quotes, so the template must not add its own). -
Undefined variables are sent literally. If
bookIdwas never set (an extractor failed), the server receives the text${bookId}, usually answers 400, and the error looks like a server problem. Give extractors a visible default value (BOOK_ID_NOT_FOUND) and assert on the status code.
Large bodies can live in a file and be read once with ${__FileToString(loan-template.json,,)}; the template is
re-read on every call, so for hot requests keep it in Body Data. A body that must vary in structure rather than in
values belongs in a JSR223 PreProcessor that builds it in Groovy, covered on
Scripting with JSR223 & Groovy.
Source: Functions Reference
Content-Type and Accept headers
The HTTP Request sampler does not add Content-Type: application/json on its own. The component reference is
explicit for the methods that carry an entity: GET, DELETE, PUT and PATCH require a Content-Type, and if no file is
used, a Header Manager attached to the sampler must define it. POST behaves the same way since JMeter 5.0: the
property http.post_add_content_type_if_missing (default false, it was effectively true before 5.0) is what used to add
application/x-www-form-urlencoded to POSTs without a content type. In the 5.6.3 source, a Body Data body with no
Content-Type header is sent by the HttpClient4 implementation as a plain-text entity, so a strict API framework sees
text/plain and answers 415 Unsupported Media Type.
The usual arrangement is one HTTP Header Manager at Thread Group level:
| Field | Meaning | JMX |
|---|---|---|
Name |
Descriptive name of the element in the tree. |
|
Name (header) |
The request header name, for example |
|
Value |
The header value. Variables and functions are allowed, for example |
|
Add / Delete |
Add a row, or delete the selected row. |
(none) |
Load / Save As |
Load a saved header table and append its entries, or save the table to a file. |
(none) |
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="JSON API headers">
<collectionProp name="HeaderManager.headers">
<elementProp name="" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
<elementProp name="" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
The component reference says several Header Managers in scope are merged into one list for the sampler, an entry with
the same name replacing the earlier one, and an empty value replacing (not removing) a header. In the 5.6.3 source the
manager closest to the sampler is merged first and keeps its value for a name both define, which is what makes "defaults
at Thread Group level, adjustments on the sampler" work: a SOAP request below can override Content-Type with its own
Header Manager. The user manual’s test-plan chapter states the opposite rule for Header Managers, as discussed under
Defaults are merged, three managers are not,
so check the headers actually sent (View Results Tree, Request tab, Request Headers) whenever a plan relies on the
override.
Two smaller points:
-
Accept: application/jsonis not required by most APIs, but content negotiation is part of production traffic and some frameworks return HTML error pages without it, which makes failures harder to read. -
A GET without a body needs no
Content-Type. A Thread-Group-levelContent-Typeis harmless on GETs with HttpClient4, because a header is only meaningful when there is an entity; keep the GETs in the same scope rather than adding a second manager to remove it (an empty value does not remove a header).
PUT, PATCH and DELETE
The methods work the same way in the sampler; what changes is where the data goes and what the API is expected to answer. Since 3.1 only POST supports multipart requests and file uploads; PUT and PATCH take their body from Body Data or from a single file with no parameter name.
| Method | Body in JMeter | Bookshelf example | Typical success code |
|---|---|---|---|
GET |
None; data in the path or the Parameters tab (query string). |
|
200 |
POST |
Body Data (JSON), with |
|
201, with a |
PUT |
Body Data with the complete representation; |
|
200 with the resource, or 204 |
PATCH |
Body Data with only the changed fields; |
|
200 or 204 |
DELETE |
Usually none; data in the path. The Parameters tab builds a query string, as for GET. |
|
204, then 404 on a later GET |
The 5.6.3 HttpClient4 implementation can also send a body with GET or DELETE when Body Data is set (it switches to an entity-enclosing request); use it only if the API really expects one, because proxies and servers often ignore or reject such bodies.
For PATCH the media type is whatever the API defines. A JSON merge patch is usually sent as
application/merge-patch+json and a JSON Patch document as application/json-patch+json; put that header in a Header
Manager under the PATCH sampler, which overrides the Thread-Group-level application/json as explained above.
Query strings and URL encoding
Search requests carry their data in the query string. There are two ways to build it, and they encode differently:
-
Parameters tab (recommended). Add rows
query=${term},page=0,size=20and tick URL Encode?. JMeter encodes each value with the HTTP sampler’s encoding (UTF-8 by default since 5.6.1) and appends?query=...&page=0&size=20for GET and DELETE. -
Inline in the Path.
/api/books?query=${term}works only for safe values. The component reference warns that the path is not encoded apart from replacing spaces with%20, so reserved or unsafe characters can cause errors such asURISyntaxException. If the path must be built inline, encode the value yourself with${__urlencode(${term})}.
How the term war & peace ends up on the wire:
| Built with | Sent as | Result |
|---|---|---|
Parameters tab, URL Encode? ticked |
|
Correct: one parameter whose value is |
Path |
|
Wrong: the |
Path with |
|
Correct. |
__urlencode uses Java’s URLEncoder, which produces application/x-www-form-urlencoded text: spaces become +.
Servers decode + as a space in query strings; in a path segment + is literal, so encode path ids (rare in
practice, ids are usually safe) with care. The Include Equals? column controls whether name= is sent for an empty
value; some APIs treat ?q= and ?q differently.
The query-string parameters in the .jmx:
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET /api/books">
<stringProp name="HTTPSampler.path">/api/books</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="query" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">true</boolProp>
<stringProp name="Argument.name">query</stringProp>
<stringProp name="Argument.value">${term}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
<boolProp name="HTTPArgument.use_equals">true</boolProp>
</elementProp>
<elementProp name="size" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">true</boolProp>
<stringProp name="Argument.name">size</stringProp>
<stringProp name="Argument.value">20</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
<boolProp name="HTTPArgument.use_equals">true</boolProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
<hashTree/>
JMeter 6.0 plans (per changes.xml on master) to let each HTTP argument row be enabled or disabled individually in the
sampler UI, which helps when experimenting with optional query parameters.
Checking API responses
An API is a contract: the status code, the content type and the shape of the body are all part of it. JMeter checks the first automatically and the rest only if assertions are added.
Status codes
By default the HTTP sampler marks responses with a status in the 4xx and 5xx ranges as unsuccessful and everything else
as successful. That catches server errors but not contract violations: a 200 where the API should answer 201, or a
302 to a login page, passes. A Response Assertion on the response code makes the expectation explicit.
| Field | Meaning | JMX |
|---|---|---|
Name |
Descriptive name in the tree. |
|
Apply to |
Main sample only, Sub-samples only, Main sample and sub-samples, or JMeter Variable Name to use. For an API call, the main sample. |
|
Field to Test |
Text Response, Request Data, Response Code, Response Message, Response Headers, Request Headers, URL Sampled, Document (text). For status checks, Response Code. |
|
Ignore Status |
Sets the sample to successful before evaluating the patterns, so a 4xx or 5xx can be asserted as expected. It clears earlier assertion failures, so use it only on the first assertion of a sampler. |
|
Pattern Matching Rules |
Contains and Matches (regular expressions), Equals and Substring (plain, case-sensitive text), plus Not to invert and Or to pass when any pattern matches instead of all. |
|
Patterns to Test |
One or more patterns, checked in turn. |
|
Custom failure message |
Replaces the generated failure message. |
|
201 Created on the loan request:
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Status is 201">
<collectionProp name="Asserion.test_strings">
<stringProp name="49587">201</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message">POST /api/loans must answer 201 Created</stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
An expected error, such as 404 after the loan was deleted, needs Ignore Status so the 4xx does not fail the sample
before the assertion runs:
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Deleted loan is 404">
<collectionProp name="Asserion.test_strings">
<stringProp name="51512">404</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">true</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
To accept a family of codes (200 or 204 after a PUT), use Matches with the pattern 20[04]
(Assertion.test_type 1), or Equals with Or and two patterns (Assertion.test_type 40).
JSON Assertion
The JSON Assertion first parses the response and fails if it is not JSON, then evaluates a JSONPath expression and
fails if the path is not found, and finally, if asked, compares the value found with an expected value. If the path
returns an array, the assertion passes when any element matches; [] asserts an empty array; an object is converted
to a string before comparison.
| Field | Meaning | JMX |
|---|---|---|
Assert JSON Path exists |
The JSONPath expression, for example |
|
Additionally assert value |
Also compare the value found; without it the assertion only checks that the path exists. |
|
Match as regular expression |
Treat Expected Value as a regular expression. In the 5.6.3 source the property defaults to |
|
Expected Value |
The value, or the regular expression, to compare with. |
|
Expect null |
Pass when the value found is |
|
Invert assertion (will fail if above conditions met) |
Negate the result, for example to fail when an |
|
Two documented behaviours catch people out. With an indefinite path (one with a wildcard, filter or deep scan, such as
$.items[*].id), an assertion without a value used to pass even when nothing matched; since JMeter 5.5 it fails if
the path is indefinite, the result is an empty list and no value is asserted. And with Match as regular expression
on, an expected value like 1.5 matches 105, because . is a regex metacharacter.
The loan must come back active:
<JSONPathAssertion guiclass="JSONPathAssertionGui" testclass="JSONPathAssertion" testname="Loan is ACTIVE">
<stringProp name="JSON_PATH">$.status</stringProp>
<stringProp name="EXPECTED_VALUE">ACTIVE</stringProp>
<boolProp name="JSONVALIDATION">true</boolProp>
<boolProp name="EXPECT_NULL">false</boolProp>
<boolProp name="INVERT">false</boolProp>
<boolProp name="ISREGEX">false</boolProp>
</JSONPathAssertion>
<hashTree/>
The JSON JMESPath Assertion (since 5.2) does the same with a JMESPath expression instead of JSONPath; its fields are the same apart from the expression language. The full catalogue of assertions, and how to keep their cost down, is on Assertions.
JSON Extractor
The JSON Extractor (renamed from JSON Path PostProcessor in 3.1) reads values from a JSON response into variables. It is the standard way to carry ids and tokens from one API call to the next. Its fields, in brief (the detailed treatment with multiple expressions and ForEach loops is on Correlation & Extractors):
| Field | Meaning | JMX |
|---|---|---|
Apply to |
Main sample, sub-samples, both, or a JMeter variable. |
|
Names of created variables |
Semicolon-separated variable names, one per expression. |
|
JSON Path expressions |
Semicolon-separated JSONPath expressions, one per variable. |
|
Default Values |
Semicolon-separated defaults used when an expression finds nothing. |
|
Match No. (0 for Random) |
|
|
Compute concatenation var (suffix _ALL) |
Also store all matches joined with |
|
The new loan id, read from the 201 response of POST /api/loans:
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="Extract loanId">
<stringProp name="JSONPostProcessor.referenceNames">loanId</stringProp>
<stringProp name="JSONPostProcessor.jsonPathExprs">$.id</stringProp>
<stringProp name="JSONPostProcessor.match_numbers">1</stringProp>
<stringProp name="JSONPostProcessor.defaultValues">LOAN_ID_NOT_FOUND</stringProp>
</JSONPostProcessor>
<hashTree/>
The Location header of a 201 holds the same id as a URL. A Boundary or Regular Expression Extractor on the
Response Headers field can read it when the body is empty; for Bookshelf the body carries the id, so the JSON
Extractor is simpler.
The Bookshelf CRUD flow
This section assembles the elements above into one complete, runnable flow over the loan resource. For this page the
Bookshelf API’s loan resource is assumed to accept, besides the POST /api/loans and GET /api/loans/{id} used
throughout the section, PUT /api/loans/{id} (replace), PATCH /api/loans/{id} (change the due date) and
DELETE /api/loans/{id} (return the book). Point it only at the local Bookshelf stack from the Docker Compose file, or
at another system you own.
The plan tree, as it appears in the GUI:
Test Plan bookshelf-crud.jmx
└── Thread Group (threads, rampup, duration from properties)
├── HTTP Request Defaults host=${__P(host,localhost)} port=${__P(port,8080)}
├── HTTP Header Manager Content-Type / Accept: application/json, Authorization: Bearer ${token}
├── CSV Data Set Config users.csv (username, password)
├── Once Only Controller
│ └── POST /api/auth/login + JSON Extractor token
├── Transaction Controller "Loan CRUD"
│ ├── POST /api/loans + JSON Extractor loanId, Response Assertion 201, JSON Assertion ACTIVE
│ ├── GET /api/loans/${loanId} + JSON Assertion bookId
│ ├── PUT /api/loans/${loanId} + Response Assertion 20[04]
│ ├── PATCH /api/loans/${loanId} + JSON Assertion dueDate
│ ├── DELETE /api/loans/${loanId} + Response Assertion 204
│ └── GET /api/loans/${loanId} + Response Assertion 404 (Ignore Status)
└── Uniform Random Timer think time between requests
The Authorization header is sent on the login request too, with an unresolved ${token} on the first iteration;
the Bookshelf login endpoint ignores it. Where an API rejects that, move the header into a Header Manager under the
Transaction Controller; the token flow itself is the subject of
HTTP Sessions, Headers, Cookies & Authentication.
The complete .jmx, kept small (listeners are left out because a CLI run writes the JTL file with -l). The book id
comes from a property here so that the file runs on its own; in bookshelf-load.jmx it comes from the search results.
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="bookshelf-crud"/>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Loan CRUD users">
<stringProp name="ThreadGroup.num_threads">${__P(threads,1)}</stringProp>
<stringProp name="ThreadGroup.ramp_time">${__P(rampup,1)}</stringProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.duration">${__P(duration,30)}</stringProp>
<stringProp name="ThreadGroup.on_sample_error">startnextloop</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController">
<intProp name="LoopController.loops">-1</intProp>
<boolProp name="LoopController.continue_forever">false</boolProp>
</elementProp>
</ThreadGroup>
<hashTree>
<ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP Request Defaults">
<stringProp name="HTTPSampler.domain">${__P(host,localhost)}</stringProp>
<stringProp name="HTTPSampler.port">${__P(port,8080)}</stringProp>
<stringProp name="HTTPSampler.protocol">http</stringProp>
</ConfigTestElement>
<hashTree/>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="JSON API headers">
<collectionProp name="HeaderManager.headers">
<elementProp name="" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
<elementProp name="" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
<elementProp name="" elementType="Header">
<stringProp name="Header.name">Authorization</stringProp>
<stringProp name="Header.value">Bearer ${token}</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="users.csv">
<stringProp name="filename">users.csv</stringProp>
<stringProp name="variableNames">username,password</stringProp>
<boolProp name="ignoreFirstLine">true</boolProp>
<stringProp name="delimiter">,</stringProp>
<boolProp name="recycle">true</boolProp>
<boolProp name="stopThread">false</boolProp>
<stringProp name="shareMode">shareMode.all</stringProp>
</CSVDataSet>
<hashTree/>
<OnceOnlyController guiclass="OnceOnlyControllerGui" testclass="OnceOnlyController" testname="Login once per user"/>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="POST /api/auth/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 name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{"username": "${username}", "password": "${password}"}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
<hashTree>
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="Extract token">
<stringProp name="JSONPostProcessor.referenceNames">token</stringProp>
<stringProp name="JSONPostProcessor.jsonPathExprs">$.token</stringProp>
<stringProp name="JSONPostProcessor.match_numbers">1</stringProp>
<stringProp name="JSONPostProcessor.defaultValues">TOKEN_NOT_FOUND</stringProp>
</JSONPostProcessor>
<hashTree/>
</hashTree>
</hashTree>
<TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="Loan CRUD">
<boolProp name="TransactionController.parent">false</boolProp>
<boolProp name="TransactionController.includeTimers">false</boolProp>
</TransactionController>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="POST /api/loans">
<stringProp name="HTTPSampler.path">/api/loans</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{"bookId": "${__P(bookId,42)}", "dueDate": "${__timeShift(yyyy-MM-dd,,P21D,,)}"}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
<hashTree>
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="Extract loanId">
<stringProp name="JSONPostProcessor.referenceNames">loanId</stringProp>
<stringProp name="JSONPostProcessor.jsonPathExprs">$.id</stringProp>
<stringProp name="JSONPostProcessor.match_numbers">1</stringProp>
<stringProp name="JSONPostProcessor.defaultValues">LOAN_ID_NOT_FOUND</stringProp>
</JSONPostProcessor>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Status is 201">
<collectionProp name="Asserion.test_strings">
<stringProp name="49587">201</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
<JSONPathAssertion guiclass="JSONPathAssertionGui" testclass="JSONPathAssertion" testname="Loan is ACTIVE">
<stringProp name="JSON_PATH">$.status</stringProp>
<stringProp name="EXPECTED_VALUE">ACTIVE</stringProp>
<boolProp name="JSONVALIDATION">true</boolProp>
<boolProp name="EXPECT_NULL">false</boolProp>
<boolProp name="INVERT">false</boolProp>
<boolProp name="ISREGEX">false</boolProp>
</JSONPathAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET /api/loans/{id}">
<stringProp name="HTTPSampler.path">/api/loans/${loanId}</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<hashTree>
<JSONPathAssertion guiclass="JSONPathAssertionGui" testclass="JSONPathAssertion" testname="Same book">
<stringProp name="JSON_PATH">$.bookId</stringProp>
<stringProp name="EXPECTED_VALUE">${__P(bookId,42)}</stringProp>
<boolProp name="JSONVALIDATION">true</boolProp>
<boolProp name="EXPECT_NULL">false</boolProp>
<boolProp name="INVERT">false</boolProp>
<boolProp name="ISREGEX">false</boolProp>
</JSONPathAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="PUT /api/loans/{id}">
<stringProp name="HTTPSampler.path">/api/loans/${loanId}</stringProp>
<stringProp name="HTTPSampler.method">PUT</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{"bookId": "${__P(bookId,42)}", "dueDate": "${__timeShift(yyyy-MM-dd,,P21D,,)}", "status": "ACTIVE"}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
<hashTree>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Status is 200 or 204">
<collectionProp name="Asserion.test_strings">
<stringProp name="1478545372">20[04]</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">1</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="PATCH /api/loans/{id}">
<stringProp name="HTTPSampler.path">/api/loans/${loanId}</stringProp>
<stringProp name="HTTPSampler.method">PATCH</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{"dueDate": "${__timeShift(yyyy-MM-dd,,P28D,,newDueDate)}"}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
<hashTree>
<JSONPathAssertion guiclass="JSONPathAssertionGui" testclass="JSONPathAssertion" testname="Due date changed">
<stringProp name="JSON_PATH">$.dueDate</stringProp>
<stringProp name="EXPECTED_VALUE">${newDueDate}</stringProp>
<boolProp name="JSONVALIDATION">true</boolProp>
<boolProp name="EXPECT_NULL">false</boolProp>
<boolProp name="INVERT">false</boolProp>
<boolProp name="ISREGEX">false</boolProp>
</JSONPathAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="DELETE /api/loans/{id}">
<stringProp name="HTTPSampler.path">/api/loans/${loanId}</stringProp>
<stringProp name="HTTPSampler.method">DELETE</stringProp>
</HTTPSamplerProxy>
<hashTree>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Status is 204">
<collectionProp name="Asserion.test_strings">
<stringProp name="49590">204</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET deleted loan">
<stringProp name="HTTPSampler.path">/api/loans/${loanId}</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<hashTree>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Deleted loan is 404">
<collectionProp name="Asserion.test_strings">
<stringProp name="51512">404</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">true</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
</hashTree>
<UniformRandomTimer guiclass="UniformRandomTimerGui" testclass="UniformRandomTimer" testname="Think time 1-3 s">
<stringProp name="ConstantTimer.delay">1000</stringProp>
<stringProp name="RandomTimer.range">2000</stringProp>
</UniformRandomTimer>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
Details worth noticing in the file:
-
The PATCH body calls
__timeShiftwith a fifth argument,newDueDate, which stores the value it generated in a variable, so the JSON Assertion can compare the response with exactly what was sent. -
The Transaction Controller sets
TransactionController.includeTimersexplicitly. The GUI writesfalsefor new controllers, but the 5.6.3 source reads a missing property astrue, so a hand-written plan should always set it. -
The thread group reads
threads,rampupanddurationwith__P, so the same file serves the smoke run below and a load run with-Joverrides. -
Action to be taken after a Sampler error is Start Next Thread Loop: if the POST fails there is no loan to read, update or delete, and continuing would only add misleading 404s.
A smoke run with one user, against the local Bookshelf stack:
jmeter -n -t bookshelf-crud.jmx -l crud.jtl -Jthreads=1 -Jrampup=1 -Jduration=30 -Jhost=localhost -Jport=8080
Creating summariser <summary>
Created the tree successfully using bookshelf-crud.jmx
Starting standalone test @ 2026 Oct 14 11:00:00 CEST (1791968400000)
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary + 14 in 00:00:29 = 0.5/s Avg: 38 Min: 9 Max: 161 Err: 0 (0.00%) Active: 1 Started: 1 Finished: 0
summary = 14 in 00:00:29 = 0.5/s Avg: 38 Min: 9 Max: 161 Err: 0 (0.00%)
Tidying up ... @ 2026 Oct 14 11:00:30 CEST (1791968430000)
... end of run
The output is representative; counts and times depend on the machine. Err: 0 is the number to check first: it
includes every failed assertion, so a zero here means each status code and JSON check above passed. The summariser
ignores transaction samples by default, so the 14 samples are HTTP requests only: one login, two full iterations of
six requests and the first request of a third, cut off when the 30 s duration ends. With the 1 s to 3 s think time
before every request, each request costs about 2 s of wall time, which is why one user sends only about one request
every two seconds.
Source: Transaction Controller (Component Reference), __timeShift (Functions) and Getting Started (User Manual)
Worked numbers: why the DELETE belongs in the load test
The CRUD flow above is a functional check. In the load plan, the borrow journey of the Bookshelf mix (60 % search only, 30 % browse and borrow, 10 % login-heavy) creates a loan on every pass, and the DELETE that returns the book is what keeps the data stable. With the standard load profile (50 threads, 60 s ramp-up, 660 s duration, so a 10-minute hold), assume an average response time of 0.15 s, the section’s average think time of 3 s per request (Z = 3 s), and these requests per iteration: search only 2, borrow 6 (search, view, create, view loan, extend, return), login-heavy 3.
-
Throughput at full load, by Little’s Law (see Performance Testing Fundamentals): 50 / (0.15 + 3) ≈ 15.9 requests/s.
-
Average requests per iteration: 0.6 x 2 + 0.3 x 6 + 0.1 x 3 = 3.3, so ≈ 15.9 / 3.3 ≈ 4.8 iterations/s.
-
Loans created: 30 % of iterations, ≈ 1.44 loans/s, or ≈ 870 loans over the 10-minute hold (plus about half a minute’s worth during the ramp-up).
Without the DELETE, about 870 active loans pile up in a single run and the books' available copies run out: later
iterations get 409 Conflict instead of 201, the error rate climbs for reasons that have nothing to do with
performance, and the next run starts from a different database. With it, a loan lives only from its POST to its
DELETE, four requests or about 4 x 3.15 ≈ 12.6 s, so by Little’s Law about 1.44 x 12.6 ≈ 18 loans are open at any
moment. A soak test makes the difference larger still. Test-data hygiene is discussed in full on
Parameterization & Test Data.
GraphQL HTTP Request
What it is
The GraphQL HTTP Request, added in JMeter 5.4, is not a new protocol implementation. The component reference calls it
a GUI variation of the HTTP Request: it shows three editors (Query, Variables and Operation Name) and converts
them into ordinary HTTP arguments of the same sampler. In the .jmx it is an HTTPSamplerProxy with
guiclass="GraphQLHTTPSamplerGui"; at run time it is executed exactly like an HTTP Request.
Compared with the HTTP Request it hides or changes these parts of the GUI:
-
Method: only POST and GET, as in the GraphQL-over-HTTP specification; POST is the default.
-
Parameters and Body Data tabs: replaced by the Query and Variables editors.
-
Files Upload tab: removed, as irrelevant to GraphQL queries.
-
Embedded Resources from HTML Files (Advanced tab): hidden, because GraphQL responses are JSON.
Everything else (server, port, protocol, path, implementation, timeouts, redirects, keep-alive, proxy, source address) is the HTTP Request’s and behaves the same.
Add it with Add → Sampler → GraphQL HTTP Request.
Fields
The GraphQL-specific fields:
| Field | Meaning | Required | JMX property (GUI state) |
|---|---|---|---|
Query |
The GraphQL query (or mutation) document. |
Yes |
|
Variables |
The operation’s variables as a valid JSON object. If the text is not valid JSON it is ignored, with an ERROR in the log. |
No |
|
Operation Name |
The operation to execute, for documents that contain several operations. |
No |
|
The inherited fields used most with GraphQL:
| Field | Use with GraphQL |
|---|---|
Server Name or IP, Port Number, Protocol |
Usually empty, inherited from HTTP Request Defaults. |
Path |
The single GraphQL endpoint, |
Method |
POST (default) or GET. |
Content encoding |
The body charset; empty means UTF-8 since 5.6.1. |
Connect / Response timeouts (Advanced tab) |
Worth setting: an expensive query that never returns otherwise holds the thread. |
POST: the default
With POST, the GUI serialises the three fields into one JSON body and stores it as Body Data. In the 5.6.3 source the
body is written as an object with the keys operationName (null when the field is empty), variables (left out when
the field is empty, copied verbatim otherwise) and query (the trimmed document). The Bookshelf search as a GraphQL
operation:
Query:
query SearchBooks($query: String!, $first: Int) {
searchBooks(query: $query, first: $first) {
id
title
author
availableCopies
}
}
Variables:
{"query": "${term}", "first": 10}
Operation Name: SearchBooks.
JMeter variables and functions are allowed in the three fields (fixed in 5.5 by bug 65108); the body is generated in the
GUI and substitution happens at run time, as for any Body Data. Keep ${...} references inside JSON strings in the
Variables field ("query": "${term}"), so that the field is valid JSON in the GUI; a bare "first": ${pageSize} is
not valid JSON until run time. For a number that must vary, send it as a string only if the schema accepts it, or build
the body with Body Data in a plain HTTP Request.
The request still needs Content-Type: application/json, the media type of a GraphQL POST body in the GraphQL-over-HTTP
specification. The GraphQL sampler does not add it (the same HttpClient4 rule as for any Body Data applies, so the body
would otherwise go out as text/plain). The Thread-Group-level Header Manager of the REST plan covers it.
The resulting .jmx fragment. The GraphQLHTTPSampler.* properties are what the GUI displays; the
HTTPsampler.Arguments element is what is actually sent.
<HTTPSamplerProxy guiclass="GraphQLHTTPSamplerGui" testclass="HTTPSamplerProxy" testname="GraphQL SearchBooks">
<stringProp name="HTTPSampler.path">/graphql</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">false</boolProp>
<stringProp name="GraphQLHTTPSampler.operationName">SearchBooks</stringProp>
<stringProp name="GraphQLHTTPSampler.query">query SearchBooks($query: String!, $first: Int) {
searchBooks(query: $query, first: $first) { id title author availableCopies }
}</stringProp>
<stringProp name="GraphQLHTTPSampler.variables">{"query": "${term}", "first": 10}</stringProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{"operationName":"SearchBooks","variables":{"query": "${term}", "first": 10},"query":"query SearchBooks($query: String!, $first: Int) {\n searchBooks(query: $query, first: $first) { id title author availableCopies }\n}"}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
<boolProp name="HTTPArgument.use_equals">true</boolProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
<hashTree/>
Because the body is generated by the GUI, editing only GraphQLHTTPSampler.query in a text editor or a script does not
change the request: the arguments still hold the old body until the sampler is opened and saved in the GUI again.
Generate GraphQL samplers in the GUI, or, when plans are produced by code, write the arguments too.
GET
Switching Method to GET makes the GUI produce query-string parameters instead of a body: operationName (when set),
query (with every run of whitespace collapsed to a single space) and variables (when set), each with URL Encode?
on. The request line for the same operation looks like this (shortened):
GET /graphql?operationName=SearchBooks&query=query+SearchBooks%28%24query%3A+String%21%2C+%24first%3A+Int%29+%7B+...&variables=%7B%22query%22%3A+%22tolkien%22%2C+%22first%22%3A+10%7D HTTP/1.1
GET has its place in a load test only if production clients use it: the GraphQL-over-HTTP specification makes GET support optional and conventionally restricted to queries, never mutations, and a server may not accept it at all. Its main effect is that responses become cacheable by HTTP caches and CDNs, so a GET-based test that hits a cache measures the cache. The trade-offs are explained in Serving GraphQL over HTTP.
Operation name and multi-operation documents
A document can define several named operations; the server executes the one named in operationName. That lets one
Query editor hold a small library, with each sampler choosing its operation:
query SearchBooks($query: String!) { searchBooks(query: $query, first: 10) { id title } }
query BookDetails($id: ID!) { book(id: $id) { id title author availableCopies } }
mutation CreateLoan($bookId: ID!) { createLoan(bookId: $bookId) { id status dueDate } }
Sending the whole document on every request costs bandwidth and server parse time, so for a load test prefer one operation per sampler and use the operation name for readability in the results. How variables, fragments and directives work on the server side is covered in Variables, Directives & Fragments.
Checking GraphQL responses
A GraphQL server served as application/json conventionally answers 200 for every request that reached the
GraphQL layer, including one whose data is null and whose errors array is full. JMeter’s default success rule
(non-4xx, non-5xx) therefore counts failed operations as successes. Two JSON Assertions fix this:
| Purpose | JSON path | Assert value | Expected | Invert |
|---|---|---|---|---|
The operation produced data |
|
No |
(none) |
No |
No GraphQL errors |
|
No |
(none) |
Yes |
The second assertion fails the sample when an errors field exists. A partial result (data plus errors) then counts as
an error, which is the right default for a load test; relax it only for operations where partial results are
expected. Servers that answer with application/graphql-response+json (when the client sends that Accept) also use
4xx codes for requests that fail validation; the assertions above still apply. The response shape is covered in
Response and Error Handling.
<JSONPathAssertion guiclass="JSONPathAssertionGui" testclass="JSONPathAssertion" testname="No GraphQL errors">
<stringProp name="JSON_PATH">$.errors</stringProp>
<stringProp name="EXPECTED_VALUE"></stringProp>
<boolProp name="JSONVALIDATION">false</boolProp>
<boolProp name="EXPECT_NULL">false</boolProp>
<boolProp name="INVERT">true</boolProp>
<boolProp name="ISREGEX">false</boolProp>
</JSONPathAssertion>
<hashTree/>
Recording GraphQL traffic
The HTTP(S) Test Script Recorder has a Detect GraphQL Request option (in its GraphQL HTTP Sampler settings). In the
5.6.3 source, a recorded POST with an application/json content type, or a GET with GraphQL parameters, is turned into
a GraphQL HTTP Request when its query starts with the keyword query or mutation; other requests stay plain HTTP
Requests. Anonymous shorthand documents ({ books { id } }) are therefore not detected, and neither are documents
that start with a fragment definition; those can be converted by hand. Recording itself is covered on
Recording & cURL Import.
GraphQL-specific load considerations
-
The query is the workload. One endpoint serves very different costs: a list of ten titles and a nested query that fetches every loan of every book’s borrowers hit the same URL. Model the operations and their selection sets from production traffic (operation names in server logs or APM), not from a single sample query. The server-side causes of expensive queries (N+1 resolver calls, unbounded lists) are discussed in Performance & N+1.
-
Label by operation. All GraphQL requests share one path, so the sampler names (
GraphQL SearchBooks,GraphQL CreateLoan) are the only way to tell them apart in the results. -
Demand control will reject a load test that does not look like real clients. Depth limits, cost limits and persisted-query allow-lists exist on many servers; a test that sends ad-hoc documents can be rejected with errors that look like failures. See Security and Demand Control.
-
Subscriptions are out of scope. GraphQL subscriptions run over WebSocket or server-sent events, which core JMeter does not support; WebSocket samplers come from plugins, covered in plain words on the other-samplers and plugins pages later in this guide.
SOAP web services
The SOAP samplers are gone
The books test SOAP with dedicated samplers. Both were removed from core JMeter:
| Element | Removed in | Replacement |
|---|---|---|
WebService(SOAP) Request |
3.0 (deprecated in 2.13) |
HTTP Request with the SOAP envelope in Body Data. |
SOAP/XML-RPC Request |
3.2 (bug 60727) |
HTTP Request; the change log points to Building a WebService Test Plan. |
The replacement is not a workaround; it is how the user manual builds SOAP tests today. Start from File → Templates…
→ Building a SOAP WebService Test Plan, which creates User Defined Variables, HTTP Request Defaults, a Thread Group, an
HTTP Request with an envelope in Body Data, an HTTP Header Manager with Content-Type and SOAPAction, and a Response
Assertion. Adjust the server in the Defaults, the path in the request, the SOAPAction header and the envelope.
Building a SOAP request
Bookshelf is a REST and GraphQL service, so the example uses a hypothetical legacy catalogue service in the same stack,
/soap/catalog, with the namespace urn:bookshelf:catalog:v1 and an operation GetBook. The steps are the same for
any SOAP 1.1 service:
-
Add → Sampler → HTTP Request. Method
POST, Path/soap/catalog. -
Paste the envelope into Body Data, with
${bookId}where the value varies. -
Under the sampler, Add → Config Element → HTTP Header Manager with
Content-Type: text/xml; charset=utf-8and theSOAPActionthe WSDL declares for the operation (in quotes, as in the template). This manager overrides the Thread-Group-levelapplication/json. -
Add an XPath2 Extractor and an XPath2 Assertion (below), and a Response Assertion on the response code.
The user manual adds two practical notes: if the service returns errors, check that the envelope’s xmlns attributes
match the WSDL exactly; and many non-.NET services do not use SOAPAction, in which case remove the header. For SOAP
1.2 the content type is application/soap+xml instead, with the action carried as its action parameter, as defined
by the W3C SOAP 1.2 specification; check which version the WSDL binding uses.
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="SOAP GetBook">
<stringProp name="HTTPSampler.path">/soap/catalog</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value"><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cat="urn:bookshelf:catalog:v1">
<soap:Body>
<cat:GetBook>
<cat:id>${bookId}</cat:id>
</cat:GetBook>
</soap:Body>
</soap:Envelope></stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="SOAP 1.1 headers">
<collectionProp name="HeaderManager.headers">
<elementProp name="" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">text/xml; charset=utf-8</stringProp>
</elementProp>
<elementProp name="" elementType="Header">
<stringProp name="Header.name">SOAPAction</stringProp>
<stringProp name="Header.value">"urn:bookshelf:catalog:v1/GetBook"</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
</hashTree>
The envelope is XML inside XML, so the .jmx stores it escaped (<, "); the GUI shows it as plain XML. A
value inserted into the envelope must be XML-safe: use ${__escapeXml(${title})} for free text.
XPath2 Extractor
The XPath2 Extractor (since 5.0) reads values from XML with XPath 2.0. The component reference recommends it over the older XPath Extractor for its easier namespace handling, better performance and XPath 2.0 support.
| Field | Meaning | Required | JMX |
|---|---|---|---|
Name |
Descriptive name in the tree. |
No |
|
Apply to |
Main sample, sub-samples, both, or a JMeter variable. Matching is applied to all qualifying samples in turn. |
Yes |
|
Return entire XPath fragment instead of text content? |
Return the matched element with its tags ( |
Yes |
|
Name of created variable |
The variable that receives the result. |
Yes |
|
XPath Query |
An XPath 2.0 expression; can return several matches. |
Yes |
|
Match No. (0 for Random) |
|
No |
|
Default Value |
Returned when nothing matches, and when the node has no value and the fragment option is off. |
Yes |
|
Namespaces aliases list |
One |
No |
|
The book title from the GetBookResponse:
<XPath2Extractor guiclass="XPath2ExtractorGui" testclass="XPath2Extractor" testname="Extract title">
<stringProp name="XPathExtractor2.default">TITLE_NOT_FOUND</stringProp>
<stringProp name="XPathExtractor2.refname">title</stringProp>
<stringProp name="XPathExtractor2.matchNumber">1</stringProp>
<stringProp name="XPathExtractor2.xpathQuery">//cat:GetBookResponse/cat:book/cat:title/text()</stringProp>
<stringProp name="XPathExtractor2.namespaces">cat=urn:bookshelf:catalog:v1</stringProp>
<boolProp name="XPathExtractor2.fragment">false</boolProp>
</XPath2Extractor>
<hashTree/>
Namespaces are the usual source of "nothing matched": a query without a prefix (//title) does not match an element
in a namespace. Declare a prefix for every namespace used in the query. A global mapping can also be kept in a
properties file named by the xpath.namespace.config property. Compiled queries are cached; the cache size is
xpath2query.parser.cache.size (default 400).
XPath2 Assertion and SOAP faults
The XPath2 Assertion (since 5.2) checks that a document is well-formed and that an XPath expression matches; the default
expression / matches any well-formed document. It also accepts boolean expressions such as count(//*error)=2.
| Field | Meaning | Required | JMX |
|---|---|---|---|
Apply to |
Main sample, sub-samples, both, or a JMeter variable. |
Yes |
|
XPath2 Assertion |
The XPath 2.0 expression to match. |
Yes |
|
Invert assertion |
Fail if the expression matches (or returns true), succeed otherwise. |
No |
|
Namespaces aliases list |
One |
No |
|
A SOAP 1.1 service reports errors with a soap:Fault element in the body, usually with HTTP status 500, which the HTTP
sampler already marks as failed. A service that returns faults with status 200, or a negative test that expects a fault,
needs an explicit check. The assertion below fails the sample whenever the response contains a fault:
<XPath2Assertion guiclass="XPath2AssertionGui" testclass="XPath2Assertion" testname="No SOAP fault">
<stringProp name="XPath.xpath">//soap:Fault</stringProp>
<boolProp name="XPath.negate">true</boolProp>
<stringProp name="XPath.namespaces">soap=http://schemas.xmlsoap.org/soap/envelope/</stringProp>
</XPath2Assertion>
<hashTree/>
For a negative test that expects a fault, add first a Response Assertion on the response code with Ignore Status
and the pattern 500, then an XPath2 Assertion on //soap:Fault/faultcode without Invert.
A plain Response Assertion with Substring on a closing tag (as the SOAP template does) is cheaper than an XPath2 Assertion because it does not parse the document. Use XPath when the structure matters, substring checks when a marker is enough.
OpenAPI-driven plan design
JMeter has no OpenAPI (Swagger) importer. Neither the user manual nor the component reference describes one; the only import paths core JMeter offers are the HTTP(S) Test Script Recorder and Tools → Import from cURL. Designing a plan from an OpenAPI contract is therefore a manual process, and a worthwhile one: the contract says what the API can do, but only the workload model says what users actually do, and a load test needs the second.
Each part of the contract maps to a JMeter element:
| OpenAPI | JMeter | Notes |
|---|---|---|
|
HTTP Request Defaults with |
Never hard-code a production URL; pass the target with |
|
One HTTP Request per operation that the workload uses, named |
Operations users never call (admin, health) stay out of the load mix. |
Path parameters ( |
|
An id from an earlier call is correlation; one chosen by the user is test data. |
Query parameters |
Parameters tab with URL Encode? ticked |
Required ones always; optional ones in the proportions real clients use them. |
|
Body Data JSON template with variables, plus |
Generate example values that respect formats, enums and limits. |
|
Response Assertion on Response Code |
Assert the documented success code ( |
Response schema |
JSON Assertion on the few fields the next step depends on |
Do not validate whole schemas under load; that is a functional-test job. |
|
A login request, JSON Extractor for the token, |
See the sessions and authentication page. |
|
Seed values for the CSV files |
Useful for a smoke test; real distributions come from production data. |
Two shortcuts help without being importers. API consoles such as Swagger UI typically display a cURL command for each
operation they execute, and Tools → Import from cURL turns such a command into an HTTP Request with its headers (see
Recording & cURL Import). And outside the Apache project, the
OpenAPI Generator project ships a jmeter generator whose documentation describes it as generating a JMeter .jmx
file; it is a third-party tool, not part of JMeter, and its output is a set of requests, not a workload model, so it
still needs the mix, data, correlation and assertions described above. It is not covered further here.
Where the contract itself comes from in a contract-first Spring Boot project is described in API-First Development: REST and gRPC.
Load-generator cost
API tests are usually cheap to generate: small requests, small JSON responses, no embedded resources. What makes them expensive is post-processing on every response.
| Element | Cost on the load generator |
|---|---|
HTTP Request with Body Data |
Low. Variable substitution in a short template is negligible next to the network round trip. |
|
Moderate: evaluated on every request. Prefer CSV data prepared up front, as the best-practices chapter advises. |
JSON Extractor |
Parses the whole response for each sample. Fine for small payloads; on large lists, extract only what the next step needs, and consider the Boundary Extractor for a single value in a big document. |
JSON Assertion |
Another full parse per sample. Keep one or two per sampler. |
Response Assertion on the response code |
Very low: no body parsing. The cheapest contract check. |
XPath2 Extractor / XPath2 Assertion |
Highest of the group: the response is parsed into an XML document for each element. Compiled queries are cached
( |
GraphQL HTTP Request |
Same as an HTTP Request; the JSON body is generated in the GUI, not at run time. |
The best-practices chapter of the user manual sums up the rule: use as few assertions as possible. For a 10-minute hold at ≈ 15.9 requests/s, as in the worked numbers above, each additional JSON element per request means about 9,500 extra parses per run, all on the load generator’s CPU and heap.
Source: Best Practices (User Manual)
Pitfalls
-
No
Content-Typeheader. The body goes out astext/plainand the API answers 415. Putapplication/jsonin a Thread-Group-level Header Manager and check the request headers once in View Results Tree. -
Unescaped data in JSON or XML bodies. A quote in a search term breaks the JSON, an ampersand breaks the XML. Escape with
__escapeXmlfor XML; clean data or use Groovy’sJsonOutputfor JSON. -
Query values inlined in the path. The path is not URL-encoded apart from spaces; use the Parameters tab or
__urlencode. -
Relying on "not 4xx/5xx" as success. A
200login page, a204where a body was expected, or a GraphQL200witherrorsall pass. Assert the documented status code and, for GraphQL, the absence oferrors. -
Ignore Status on a later assertion. It clears failures recorded by earlier assertions; use it only on the first assertion of a sampler.
-
JSON Assertion in regex mode by accident. Match as regular expression defaults to on in the 5.6.3 source when the property is missing; set
ISREGEXexplicitly. -
Editing GraphQL samplers outside the GUI. The request is the generated argument, not the
GraphQLHTTPSampler.queryproperty; re-save the sampler in the GUI after changing the query. -
Invalid JSON in the GraphQL Variables field. It is ignored with an ERROR in the log, and the operation runs without variables. Keep
${...}inside JSON strings. -
Creating data without cleaning it up. Loans, orders or users created under load change the database and the next run’s results; return or delete what the test creates (see the worked numbers above).
-
Copying the books' SOAP samplers. WebService(SOAP) Request and SOAP/XML-RPC Request no longer exist; plans that use them do not load in 5.6.3.
-
Expecting an OpenAPI import. There is none in JMeter; plan the requests from the contract and the workload model by hand, or use cURL import for individual operations.
From the books to 5.6 to 6.0
| Topic | Books (JMeter 2.11 to 3.0) | JMeter 5.6.3 and planned 6.0 |
|---|---|---|
SOAP |
WebService(SOAP) Request and SOAP/XML-RPC Request samplers (Cookbook ch. 4; PTwJ ch. 7). |
WebService(SOAP) Request removed in 3.0, SOAP/XML-RPC Request removed in 3.2. Use HTTP Request with Body Data and the Building a SOAP WebService Test Plan template. |
JSON extraction |
The |
Core JSON Extractor (added in 3.0 as the JSON Path PostProcessor, renamed in 3.1), JSON Assertion (4.0), JSON JMESPath Extractor and Assertion (5.2). The plugin is no longer needed. |
XML extraction |
XPath Extractor and XPath Assertion (XPath 1.0). |
XPath2 Extractor (5.0) and XPath2 Assertion (5.2), recommended over the XPath 1.0 elements. 6.0 plans Saxon-HE 13. |
POST content type |
A POST without a content type got |
Since 5.0, |
Default encoding |
Platform or ISO-8859-1 defaults; non-ASCII data needed explicit encoding. |
UTF-8 is the HTTP sampler default since 5.6.1. |
GraphQL |
Not covered (it predates GraphQL’s spread). |
GraphQL HTTP Request since 5.4; variables and functions supported since 5.5. |
JSON Assertion with indefinite paths |
Not applicable (no core JSON Assertion). |
Since 5.5, an indefinite path with an empty result and no expected value fails. |
JSONPath library |
Plugin-specific. |
Jayway JsonPath; 6.0 plans json-path 3.0.0. Re-test complex expressions when upgrading. |
JSON Extractor defaults |
Not applicable. |
6.0 plans support for trailing empty default values, so expressions like |
HTTP arguments |
All rows always sent. |
6.0 plans to allow enabling or disabling individual arguments in the sampler UI. |
Redirects |
307 and 308 redirects followed as GET by some implementations. |
6.0 plans to keep the original method (POST, PUT…) when following 307 and 308 redirects, as the HTTP specification requires. An API that answers a write with 307 will see the write repeated at the new location. |
No other change to the elements on this page is listed in the planned 6.0 change log at the time of writing.
References
-
Functions Reference (
__timeShift,__urlencode,__escapeXml,__FileToString,__groovy) -
Apache JMeter source (
xdocs/changes.xmlfor planned 6.0 changes; the 5.6.3 sources of the GraphQL sampler GUI andGraphQLRequestParamUtils,HTTPHC4Impl,HeaderManager,JSONPathAssertion, and thebin/templates/build-webservice-test-plan.jmxtemplate) -
OpenAPI Generator: jmeter generator (third-party, mentioned only)
-
Bayo Erinle, JMeter Cookbook, Packt, 2014 (JMeter 2.11), chapter 2 (JSON extraction with the
jp@gcplugin) and chapter 4 (the SOAP/XML-RPC sampler): used for the historical comparison only. -
Bayo Erinle, Performance Testing with JMeter, 2nd ed., Packt, 2015 (JMeter 2.12), chapter 7 (the SOAP sampler and the
jp@gcJSON extractor): used for the historical comparison only.