API Load Testing
Modern e-commerce runs on APIs. Your storefront talks to a Product API. The checkout calls Payment and Inventory APIs. Mobile apps hit the same backend. Testing these APIs under load is essential.
REST API Fundamentals in JMeter
HTTP Request Configuration
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy"
testname="Get Products API">
<stringProp name="HTTPSampler.domain">api.mystore.com</stringProp>
<stringProp name="HTTPSampler.port">443</stringProp>
<stringProp name="HTTPSampler.protocol">https</stringProp>
<stringProp name="HTTPSampler.path">/v1/products</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
</HTTPSamplerProxy>
HTTP Header Manager
APIs require specific headers. Add a Header Manager to your Thread Group:
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager"
testname="API Headers">
<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>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">JMeter-LoadTest/1.0</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
JWT Authentication
Most modern APIs use JWT (JSON Web Tokens) for authentication.
The JWT Flow
1. Login Request → Receive JWT Token
2. Extract Token → Store in Variable
3. All Subsequent Requests → Include Token in Header
4. Token Expires → Refresh Token
Step 1: Login and Extract Token
<HTTPSamplerProxy testname="API Login">
<stringProp name="HTTPSampler.path">/v1/auth/login</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<stringProp name="Argument.value">{
"email": "${email}",
"password": "${password}"
}</stringProp>
</HTTPSamplerProxy>
<hashTree>
<JSONPostProcessor testname="Extract Tokens">
<stringProp name="JSONPostProcessor.referenceNames">accessToken;refreshToken;tokenExpiry</stringProp>
<stringProp name="JSONPostProcessor.jsonPathExprs">$.accessToken;$.refreshToken;$.expiresIn</stringProp>
<stringProp name="JSONPostProcessor.defaultValues">NOT_FOUND;NOT_FOUND;3600</stringProp>
</JSONPostProcessor>
<JSONPathAssertion testname="Verify Login Success">
<stringProp name="JSON_PATH">$.accessToken</stringProp>
<boolProp name="EXPECT_NULL">false</boolProp>
</JSONPathAssertion>
</hashTree>
Step 2: Authorization Header Manager
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager"
testname="Auth Header">
<collectionProp name="HeaderManager.headers">
<elementProp name="Authorization" elementType="Header">
<stringProp name="Header.name">Authorization</stringProp>
<stringProp name="Header.value">Bearer ${accessToken}</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
Step 3: Token Refresh Strategy
For long-running tests, implement token refresh:
// JSR223 PreProcessor - Check and refresh token
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
def tokenExpiry = vars.get("tokenExpiryTime")?.toLong() ?: 0
def currentTime = System.currentTimeMillis()
// Refresh 5 minutes before expiry
def refreshThreshold = 5 * 60 * 1000
if (currentTime > (tokenExpiry - refreshThreshold)) {
log.info("Token expiring soon, refreshing...")
def refreshToken = vars.get("refreshToken")
def http = new URL("https://api.mystore.com/v1/auth/refresh").openConnection()
http.setRequestMethod("POST")
http.setRequestProperty("Content-Type", "application/json")
http.setDoOutput(true)
def body = JsonOutput.toJson([refreshToken: refreshToken])
http.outputStream.write(body.getBytes("UTF-8"))
if (http.responseCode == 200) {
def response = new JsonSlurper().parse(http.inputStream)
vars.put("accessToken", response.accessToken)
vars.put("refreshToken", response.refreshToken)
vars.put("tokenExpiryTime", (currentTime + (response.expiresIn * 1000)).toString())
log.info("Token refreshed successfully")
} else {
log.error("Token refresh failed: ${http.responseCode}")
}
}
Complete API Test Structure
Test Plan
├── HTTP Request Defaults (base URL, encoding)
├── HTTP Header Manager (Content-Type, Accept)
├── CSV Data Set Config (user credentials)
│
├── Thread Group: API Users
│ ├── Login Request
│ │ └── JSON Extractor (tokens)
│ │
│ ├── HTTP Header Manager (Authorization: Bearer ${token})
│ │
│ ├── Transaction: Product Operations
│ │ ├── GET /products
│ │ ├── GET /products/{id}
│ │ └── GET /products?category=electronics
│ │
│ ├── Transaction: Cart Operations
│ │ ├── POST /cart (create)
│ │ ├── POST /cart/items (add)
│ │ ├── PATCH /cart/items/{id} (update)
│ │ └── GET /cart (retrieve)
│ │
│ └── Transaction: Order Operations
│ ├── POST /orders (create)
│ ├── GET /orders/{id}
│ └── GET /orders (list)
│
└── Listeners
Testing Different HTTP Methods
GET with Query Parameters
<HTTPSamplerProxy testname="Search Products">
<stringProp name="HTTPSampler.path">/v1/products</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="category" elementType="HTTPArgument">
<stringProp name="Argument.name">category</stringProp>
<stringProp name="Argument.value">${category}</stringProp>
</elementProp>
<elementProp name="minPrice" elementType="HTTPArgument">
<stringProp name="Argument.name">minPrice</stringProp>
<stringProp name="Argument.value">10</stringProp>
</elementProp>
<elementProp name="maxPrice" elementType="HTTPArgument">
<stringProp name="Argument.name">maxPrice</stringProp>
<stringProp name="Argument.value">100</stringProp>
</elementProp>
<elementProp name="page" elementType="HTTPArgument">
<stringProp name="Argument.name">page</stringProp>
<stringProp name="Argument.value">${__Random(1,10)}</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
POST with JSON Body
<HTTPSamplerProxy testname="Create Order">
<stringProp name="HTTPSampler.path">/v1/orders</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<stringProp name="Argument.value">{
"cartId": "${cartId}",
"shippingAddress": {
"street": "${street}",
"city": "${city}",
"zipCode": "${zipCode}",
"country": "US"
},
"paymentMethod": {
"type": "credit_card",
"token": "${paymentToken}"
},
"notes": "Load test order - ${__UUID}"
}</stringProp>
</HTTPSamplerProxy>
PUT for Updates
<HTTPSamplerProxy testname="Update Cart Item">
<stringProp name="HTTPSampler.path">/v1/cart/items/${itemId}</stringProp>
<stringProp name="HTTPSampler.method">PUT</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<stringProp name="Argument.value">{
"quantity": ${__Random(1,5)},
"notes": "Updated via load test"
}</stringProp>
</HTTPSamplerProxy>
DELETE
<HTTPSamplerProxy testname="Remove Cart Item">
<stringProp name="HTTPSampler.path">/v1/cart/items/${itemId}</stringProp>
<stringProp name="HTTPSampler.method">DELETE</stringProp>
</HTTPSamplerProxy>
GraphQL Load Testing
GraphQL presents unique challenges: single endpoint, variable query complexity.
GraphQL Query Request
<HTTPSamplerProxy testname="GraphQL - Get Products">
<stringProp name="HTTPSampler.path">/graphql</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<stringProp name="Argument.value">{
"query": "query GetProducts($category: String!, $limit: Int) { products(category: $category, limit: $limit) { id name price stockLevel images { url } reviews { rating } } }",
"variables": {
"category": "${category}",
"limit": 20
}
}</stringProp>
</HTTPSamplerProxy>
GraphQL Mutation
<HTTPSamplerProxy testname="GraphQL - Add to Cart">
<stringProp name="HTTPSampler.path">/graphql</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<stringProp name="Argument.value">{
"query": "mutation AddToCart($productId: ID!, $quantity: Int!) { addToCart(productId: $productId, quantity: $quantity) { cartId items { productId quantity price } total } }",
"variables": {
"productId": "${productId}",
"quantity": ${__Random(1,3)}
}
}</stringProp>
</HTTPSamplerProxy>
<hashTree>
<JSONPostProcessor testname="Extract Cart ID">
<stringProp name="JSONPostProcessor.referenceNames">cartId</stringProp>
<stringProp name="JSONPostProcessor.jsonPathExprs">$.data.addToCart.cartId</stringProp>
</JSONPostProcessor>
</hashTree>
GraphQL-Specific Assertions
GraphQL returns 200 even for errors. Check the response structure:
<!-- Verify no GraphQL errors -->
<JSONPathAssertion testname="No GraphQL Errors">
<stringProp name="JSON_PATH">$.errors</stringProp>
<boolProp name="EXPECT_NULL">true</boolProp>
<boolProp name="INVERT">false</boolProp>
</JSONPathAssertion>
<!-- Verify data returned -->
<JSONPathAssertion testname="Data Returned">
<stringProp name="JSON_PATH">$.data</stringProp>
<boolProp name="EXPECT_NULL">false</boolProp>
</JSONPathAssertion>
Microservices Testing
E-commerce often uses microservices. Test each service independently, then together.
Service-Specific Test Plans
test-plans/
├── product-service-load.jmx
├── cart-service-load.jmx
├── payment-service-load.jmx
├── inventory-service-load.jmx
└── full-integration-load.jmx
Testing Service Communication
<!-- Product Service -->
<ThreadGroup testname="Product Service Direct">
<stringProp name="ThreadGroup.num_threads">200</stringProp>
</ThreadGroup>
<hashTree>
<ConfigTestElement testname="Product Service URL">
<stringProp name="HTTPSampler.domain">product-service.internal</stringProp>
<stringProp name="HTTPSampler.port">8080</stringProp>
</ConfigTestElement>
<HTTPSamplerProxy testname="Get Product">
<stringProp name="HTTPSampler.path">/products/${productId}</stringProp>
</HTTPSamplerProxy>
</hashTree>
<!-- Inventory Service -->
<ThreadGroup testname="Inventory Service Direct">
<stringProp name="ThreadGroup.num_threads">100</stringProp>
</ThreadGroup>
<hashTree>
<ConfigTestElement testname="Inventory Service URL">
<stringProp name="HTTPSampler.domain">inventory-service.internal</stringProp>
<stringProp name="HTTPSampler.port">8081</stringProp>
</ConfigTestElement>
<HTTPSamplerProxy testname="Check Stock">
<stringProp name="HTTPSampler.path">/inventory/${productId}/stock</stringProp>
</HTTPSamplerProxy>
<HTTPSamplerProxy testname="Reserve Stock">
<stringProp name="HTTPSampler.path">/inventory/${productId}/reserve</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<stringProp name="Argument.value">{"quantity": ${quantity}, "orderId": "${orderId}"}</stringProp>
</HTTPSamplerProxy>
</hashTree>
API-Specific Assertions
Response Schema Validation
Use JSON Schema Validator plugin or Groovy:
// JSR223 Assertion - Validate response schema
import groovy.json.JsonSlurper
def response = prev.getResponseDataAsString()
def json = new JsonSlurper().parseText(response)
// Required fields check
def requiredFields = ["id", "name", "price", "stockLevel"]
def missingFields = requiredFields.findAll { !json.containsKey(it) }
if (missingFields) {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage("Missing required fields: ${missingFields}")
return
}
// Type validation
if (!(json.price instanceof Number)) {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage("Price must be a number, got: ${json.price.getClass()}")
return
}
if (json.price < 0) {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage("Price cannot be negative: ${json.price}")
}
Rate Limit Detection
// JSR223 Assertion - Detect rate limiting
def responseCode = prev.getResponseCode()
def responseMessage = prev.getResponseMessage()
if (responseCode == "429") {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage("Rate limited! Headers: ${prev.getResponseHeaders()}")
// Extract retry-after if present
def headers = prev.getResponseHeaders()
def retryMatch = headers =~ /Retry-After:\s*(\d+)/
if (retryMatch) {
log.warn("Rate limited. Retry after: ${retryMatch[0][1]} seconds")
}
}
Key Takeaways
- Always set headers: Content-Type, Accept, and Authorization are essential for API testing.
- JWT tokens expire: Implement refresh logic for long-running load tests.
- GraphQL needs special handling: Single endpoint, check for errors in response body not just status code.
- Test microservices individually and together: Integration issues only appear when services communicate under load.
Next Lesson
Your API tests are running. Now what? How do you interpret the results? Next: Results Analysis and Bottlenecks - understanding metrics, identifying performance problems, and knowing when throughput plateaus mean trouble.