Session 6 · Checking responses with catch_response#
The big idea#
By default Locust calls a request a success unless the server returns an error status (400 and above). But a reply can be
“200 OK” and still be wrong: the page is empty, the token is missing, the answer took far too long. catch_response=True
lets you inspect the reply and mark it success or failure yourself. It also lets you treat an expected error
(such as a 404 you want to test) as a success.
Everyday example: a parcel delivery. “Delivered” on the tracking page is not enough; you open the box and check that
the right item is inside and undamaged. A 200 status is the “delivered” message, and catch_response is opening the box.
Four checks#
from locust import HttpUser, between, task
class CheckingUser(HttpUser):
wait_time = between(0.1, 0.3)
@task
def login_has_token(self):
with self.client.post("/login", json={"username": "demo", "password": "secret"}, catch_response=True) as response:
if "token" not in response.json():
response.failure("login reply has no token")
@task
def missing_product_is_expected(self):
# a 404 is normally a failure, but here it is the correct answer
with self.client.get("/products/99", name="/products/[missing]", catch_response=True) as response:
if response.status_code == 404:
response.success()
@task
def slow_page(self):
with self.client.get("/slow", catch_response=True) as response:
if response.elapsed.total_seconds() > 0.25:
response.failure(f"too slow: {response.elapsed.total_seconds():.2f}s")
@task
def flaky_page(self):
self.client.get("/flaky") # the server fails every 4th call with a 500
login_has_tokenpasses only if the JSON body contains atoken.missing_product_is_expectedasks for product 99. A 404 would normally count as a failure; here it is the correct answer, so we callresponse.success().slow_pagefails any response slower than 0.25 s. The shop’s/slowtakes 0.3 s on purpose, so this check fails every time.flaky_pagehas no check at all: the shop returns a 500 on every fourth call and Locust reports it by itself.
locust -f 06_validate.py --headless -u 4 -r 4 -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
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET /flaky 25 6(24.00%) | 2 1 8 2 | 3.19 0.77
POST /login 40 0(0.00%) | 2 1 4 3 | 5.11 0.00
GET /products/[missing] 25 0(0.00%) | 2 2 4 2 | 3.19 0.00
GET /slow 26 26(100.00%) | 303 302 308 302 | 3.32 3.32
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
Aggregated 116 32(27.59%) | 70 1 308 3 | 14.82 4.09
Error report
# occurrences Error
------------------|---------------------------------------------------------------------------------------------------------------------------------------------
2 GET /slow: too slow: 0.31s
6 GET /flaky: HTTPError('500 Server Error: Internal Server Error for url: /flaky')
24 GET /slow: too slow: 0.30s
------------------|---------------------------------------------------------------------------------------------------------------------------------------------
What the results mean#
/products/[missing]has 0 failures even though every reply was a 404, because we marked them as expected./slowshows 100% failures. The status was 200, but our rule rejected it. The Error report lists the reason text passed tofailure(), which is why a clear message such astoo slow: 0.30sis worth writing./flakyfailed about a quarter of the time (the server fails every 4th call). Locust grouped them asHTTPError('500 Server Error ...')./loginpassed every time, so the body check found the token.
Without these checks, the test would show 0% failures for /slow and for a login that returns an empty token, and you would
wrongly think the service is healthy.
Common mistakes#
Forgetting
with ... as response. Withcatch_response=Truethe request is only recorded when thewithblock ends.Calling neither
success()norfailure(). If you do nothing the status code decides, which is not what you intended.Parsing JSON without protection. If the body is not JSON,
response.json()raises an error. Wrap it, or checkresponse.textfirst.Using a vague reason. “failed” tells nobody anything. Include the status or the missing field.
Setting the threshold below the real speed of your service, as we did on purpose for
/slow. Base thresholds on what your users need.
Hands-on exercises#
Exercise 1. Change the slow_page threshold to 0.5 s. What happens to the /slow failure count?
Solution
It drops to 0, because the 0.3 s responses are now under the limit. The row still shows an average near 300 ms: passing a check does not make the request faster.
Exercise 2. Make /profile (without a token) count as a success when the server answers 401.
Solution
@task
def profile_without_login(self):
with self.client.get("/profile", name="/profile [no token]", catch_response=True) as response:
if response.status_code == 401:
response.success()
The shop returns 401 when no token is sent, so this request is recorded as a success.
Exercise 3. Fail a request if the product list is empty.
Solution
with self.client.get("/products", catch_response=True) as response:
if len(response.json()) == 0:
response.failure("empty product list")
A 200 response with [] would then be reported as a failure with that reason.
Practice#
Take the Session 6 quiz. The original page Using catch_response for Custom Response Validation is in the reference section.