FastAPI auto generated load testing with K6

Share on:

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?

  1. Launch a FastAPI sample app with a prebuilt docker image form the author of the framework.

  2. Create a k6 script from the openapi spec generated by FastAPI

  3. 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

k6s-fastapi-run

1docker-compose down