🔥 0
0
Lesson 8 of 10 25 min +200 XP

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.

🧠 Quick Quiz

Test your understanding of this lesson.

1

Why must you include the Authorization header in every API request after login?

2

What's special about GraphQL load testing compared to REST?

3

What is a token refresh strategy?

E-Commerce Load Scenarios