I've been using Locust recently to run some load tests - most significantly these tests against SQLite running with Django and this test exercising Datasette and Gunicorn.
Locust tests are defined in a locustfile.py
file. Here's the most basic possible test, which sends requests to the /
page of a web application:
from locust import HttpUser, task
class Page(HttpUser):
@task
def index(self):
self.client.get("/")
With this saved as locustfile.py
you can run it in two ways. You can start a web interface to Locust like this:
locust
This opens a web server on http://0.0.0.0:8089/ (by default) which offers an interface for starting a new test:
You can run this for as long as you like, and it will produce both statistics on the load test and some pleasing charts:
You can also run tests without the web server at all. I tend to use this option as it's quicker to repeat a test, and you can easily copy and paste the results into a GitHub issue thread.
locust --headless --users 4 --spawn-rate 2 -H http://127.0.0.1:8001
This runs the tests in the current locustfile.py
against http://127.0.0.1:8001
, with four concurrent users and ramping up at 2 users every second (so taking two seconds to ramp up to full concurrency).
Hit Ctrl+C
to end the test. It will end up producing something like this:
Type Name # reqs # fails | Avg Min Max Med | req/s failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET /fixtures/sortable 475 0(0.00%) | 169 110 483 170 | 23.58 0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
Aggregated 475 0(0.00%) | 169 110 483 170 | 23.58 0.00
Response time percentiles (approximated)
Type Name 50% 66% 75% 80% 90% 95% 98% 99% 99.9% 99.99% 100% # reqs
--------|--------------------------------------------------------------------------------|--------|------|------|------|------|------|------|------|------|------|------|------
GET /fixtures/sortable 170 170 180 180 190 200 210 250 480 480 480 475
--------|--------------------------------------------------------------------------------|--------|------|------|------|------|------|------|------|------|------|------|------
Aggregated 170 170 180 180 190 200 210 250 480 480 480 475
Locust tests can get a lot more complex than this. The documentation provides this example:
import time
from locust import HttpUser, task, between
class QuickstartUser(HttpUser):
wait_time = between(1, 5)
@task
def hello_world(self):
self.client.get("/hello")
self.client.get("/world")
@task(3)
def view_items(self):
for item_id in range(10):
self.client.get(f"/item?id={item_id}", name="/item")
time.sleep(1)
def on_start(self):
self.client.post("/login", json={"username":"foo", "password":"bar"})
This illustrates some neat concepts. Each "user" will constantly pick a task at random, where a task is a method decorated with the @task
decorator. @task(3)
here gives that task a weight of three, so it's three times more likely to be accepted.
The self.client
can maintain cookie state between requests, with each user getting a separate copy. on_start
is used here to log the user in, but also demonstrates how POST requests can work against APIs that accept JSON.
Created 2022-10-22T17:33:58-07:00 · Edit