Session 5 · Testing a web service with HttpUser#

The big idea#

HttpUser is a user that comes with an HTTP client (self.client). Every request you send through it is timed and recorded in the results, so a load test is just a script of the requests a real person would make: log in once, then browse, look at a profile, place an order.

Everyday example: a mystery shopper. A company sends someone into its own stores to walk in, ask for help, buy something and time every step. HttpUser is a mystery shopper for your website, and Locust sends hundreds of them at once.

HttpUser talks to your service Each HttpUser has a client. Requests are recorded by name, so you can group many URLs into one row of the results. HttpUser self.client.get(...) self.client.post(...) Your service /products /login Stats row #reqs · fails avg · p95 · req/s GET/POST recorded name="/products/[id]" folds /products/1, /products/2 ... into one row
Each HttpUser has a client. Requests are recorded by name, so you can group many URLs into one row of the results.

A shopper script#

import random

from locust import HttpUser, between, task


class ShopUser(HttpUser):
    wait_time = between(0.1, 0.4)

    def on_start(self):
        reply = self.client.post("/login", json={"username": "demo", "password": "secret"})
        self.client.headers["Authorization"] = "Bearer " + reply.json()["token"]

    @task(5)
    def list_products(self):
        self.client.get("/products")

    @task(3)
    def view_product(self):
        product_id = random.randint(1, 3)
        self.client.get(f"/products/{product_id}", name="/products/[id]")   # group all ids in one row

    @task(2)
    def profile(self):
        self.client.get("/profile")

    @task(1)
    def place_order(self):
        self.client.post("/orders", json={"product_id": random.randint(1, 3), "qty": 1})
  • on_start logs in once and keeps the token in the client’s headers, so every later request is authenticated.

  • Weights 5, 3, 2, 1 make browsing the common action and ordering the rare one.

  • name="/products/[id]" groups /products/1, /products/2 and /products/3 into one row. Without it you would get a separate row per URL, which is unreadable for ids.

  • json= sends a JSON body with the right header.

Run it: 5 users for 8 seconds against the practice shop.

python shop_server.py &
locust -f 05_http.py --headless -u 5 -r 5 -t 8s --host http://127.0.0.1:8765 --only-summary --loglevel WARNING
Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
POST     /login                                                                             5     0(0.00%) |      9       8      11     10 |    0.64        0.00
POST     /orders                                                                           15     0(0.00%) |      2       2       4      3 |    1.92        0.00
GET      /products                                                                         79     0(0.00%) |      2       1       6      3 |   10.14        0.00
GET      /products/[id]                                                                    36     0(0.00%) |      2       1       5      3 |    4.62        0.00
GET      /profile                                                                          26     0(0.00%) |      2       1       3      3 |    3.34        0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                       161     0(0.00%) |      2       1      11      3 |   20.66        0.00

Reading the results table#

Column

Meaning

# reqs

How many requests were made

# fails

How many failed (error status or exception)

Avg Min Max Med

Response times in milliseconds

req/s

Throughput

Notice the shape of the numbers. /login ran 5 times, once per user, because it is in on_start. /products has the most requests because it has the highest weight. Every request here took a few milliseconds because the shop runs on the same machine. Over a real network, times are far larger.

Failures. Locust counts a request as failed when the status code is 400 or above, or the connection breaks. All 161 requests succeeded here. The next session shows how to define “failed” yourself.

Web UI or headless?#

Leave out --headless and open http://localhost:8089: a web page lets you type the users, spawn rate and host and shows live charts. Headless suits scripts and CI because it prints the same table to the terminal.

Common mistakes#

  • Forgetting --host. Without it, requests have no base URL and fail. Set host = "..." in the class or pass --host.

  • Not grouping dynamic URLs. /products/17, /products/18… create thousands of rows. Use name=.

  • Logging in inside every task. Use on_start.

  • Sending real credentials or real purchases to production. Use test accounts and a safe environment.

  • Testing from the same machine as the server and trusting the numbers. Local tests are good for learning and for checking scripts, not for capacity planning.

Hands-on exercises#

Exercise 1. Add a task that requests /slow with weight 1. What happens to the average response time?

Solution
@task(1)
def slow(self):
    self.client.get("/slow")

A new row /slow appears with an average near 300 ms, and the aggregated average rises because 300 ms requests are mixed with 2 ms ones. Look at the per-row figures, not just the total.

Exercise 2. The /profile request is missing its token. What status code would you see, and how would Locust report it?

Solution

The shop returns 401, which Locust counts as a failure, so /profile would show 100% in the # fails column. (Remove the Authorization header from on_start and run it to see.)

Exercise 3. Why do /orders requests number far fewer than /products requests?

Solution

Their task weights are 1 and 5, so a user picks /products five times for each /orders. In the run above the counts were 15 and 79.

Practice#

Take the Session 5 quiz. The original pages on HttpUser and HTTP POST requests are in the reference section.