FastAPI auto generated load testing with K6
Explore k6 load testing with FastAPI auto generated openapi spec.
Overview
K6.io is a load testing tool I've started using in some projects at work with very good results. Searching to speed up the test creation process while reading k6 blog and docs ended up discovering this post and remembered FastAPI generates the Openapi spec from routes.
Fastapi is a high-performance web framework for building APIs with Python. Amazing tool and community, just give it a try.
k6 was just acquired by Grafana so expect larger adoption in the cloud native ecosystem.
What are we doing here?
Launch a FastAPI sample app with a prebuilt docker image form the author of the framework.
Create a k6 script from the openapi spec generated by FastAPI
Execute a k6 load test that came from another dimension and smell the magic.
Requirements
No packages requirements if you already have docker and docker-compose. Create these two files in your local directory (JFYI: I use linux and bash):
- docker-compose.yaml
1version: '3.8'
2
3services:
4
5 fastapi:
6 image: tiangolo/uvicorn-gunicorn-fastapi:python3.7
7 volumes:
8 - ${PWD}/main.py:/app/main.py
9 command: /start-reload.sh
10 ports:
11 - 80:80
12
13 openapi-cli:
14 image: openapitools/openapi-generator-cli
15 volumes:
16 - ${PWD}:/ci
17 command: generate -i http://fastapi/openapi.json -g k6 -o /ci/k6/ --skip-validate-spec
18
19 k6:
20 image: loadimpact/k6
21 volumes:
22 - ${PWD}:/ci
23 command: run /ci/k6/script.js
- main.py (example from FastAPI docs)
1from typing import Optional
2
3from fastapi import FastAPI, Header, HTTPException
4from pydantic import BaseModel
5
6fake_secret_token = "coneofsilence"
7
8fake_db = {
9 "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
10 "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
11}
12
13app = FastAPI()
14
15
16class Item(BaseModel):
17 id: str
18 title: str
19 description: Optional[str] = None
20
21
22@app.get("/items/{item_id}", response_model=Item)
23async def read_main(item_id: str, x_token: str = Header(...)):
24 if x_token != fake_secret_token:
25 raise HTTPException(status_code=400, detail="Invalid X-Token header")
26 if item_id not in fake_db:
27 raise HTTPException(status_code=404, detail="Item not found")
28 return fake_db[item_id]
29
30
31@app.post("/items/", response_model=Item)
32async def create_item(item: Item, x_token: str = Header(...)):
33 if x_token != fake_secret_token:
34 raise HTTPException(status_code=400, detail="Invalid X-Token header")
35 if item.id in fake_db:
36 raise HTTPException(status_code=400, detail="Item already exists")
37 fake_db[item.id] = item
38 return item
Start FastAPI app
1docker-compose up fastapi
Launch the python app and check the auto-generated docs at http://localhost/docs
Auto generate K6 script from Openapi spec
1docker-compose run -u ${UID} --rm openapi-cli
The script need to be updated/tuned after being generated. Check again the recomendations by k6 in the linked post. This is a slightly updated version of the script.
Copy this file to ${PWD}/ci/k6/script.js or check the required changes:
- Added xToken
- Update the body.id string to be random in the new items endpoint
- Stringify the request body
The fastapi app is running in reload mode if you need to change/test something.
1import http from "k6/http";
2import { group, check, sleep } from "k6";
3import { randomString } from "https://jslib.k6.io/k6-utils/1.1.0/index.js";
4
5const BASE_URL = "http://fastapi";
6const SLEEP_DURATION = 0.1;
7
8let xToken = "coneofsilence";
9let randomName = randomString(8);
10
11export default function() {
12 group("/items/", () => {
13 let url = BASE_URL + `/items/`;
14 let body = {"id": `${randomName}`, "title": "string", "description": "string"};
15 let params = {headers: {"Content-Type": "application/json", "x-token": `${xToken}`, "Accept": "application/json"}};
16 console.log(JSON.stringify(body));
17 let request = http.post(url, JSON.stringify(body), params);
18 check(request, {
19 "Successful Response": (r) => r.status === 200
20 });
21 sleep(SLEEP_DURATION);
22 });
23 group("/items/{item_id}", () => {
24 let itemId = `${randomName}`;
25 let url = BASE_URL + `/items/${itemId}`;
26 let params = {headers: {"x-token": `${xToken}`, "Accept": "application/json"}};
27 let request = http.get(url, params);
28 check(request, {
29 "Successful Response": (r) => r.status === 200
30 });
31 sleep(SLEEP_DURATION);
32 });
Execute K6 script
1docker-compose run --rm k6
1docker-compose down