Python course Β· Module 6: Async and FastAPI

Testing - verifying stability

5 min read
In this lesson4

Welcome! Darwin here with testing for FastAPI!

Imagine you fix one line in the login endpoint, and after deployment it turns out the species list has stopped working. Nobody checked, because "the change was tiny". Automated tests catch slip-ups like this in a few seconds, before anyone but you sees them.

Testing is a critical part of production applications. FastAPI has great support for pytest!

Safari analogy: Testing is like checking the gear before a safari - we make sure the GPS works, the vehicle is in good shape, the radio picks up a signal! Better to find problems before the trip!

Installation

We install three packages: pytest runs the tests, httpx is the HTTP client that FastAPI builds its test client on, and pytest-asyncio lets you write tests as async def functions:

1pip install pytest httpx pytest-asyncio

Nothing else needs configuring. pytest finds files named test_*.py or *_test.py by itself and runs the functions in them whose names start with test.

A basic FastAPI test

TestClient simulates HTTP requests to the application without starting a server - you need neither uvicorn nor a free port. You create it once, passing the app object, and the first test can check the root route:

1# test_main.py
2from fastapi.testclient import TestClient
3from main import app
4
5client = TestClient(app)
6
7def test_root():
8    response = client.get("/")
9    assert response.status_code == 200
10    assert response.json() == {"message": "Hello Safari"}

The test is an ordinary def function, even if the endpoints in the application are async - TestClient takes care of the event loop itself. The assert keyword checks a condition, and when it fails, pytest shows both compared values, so you see at once what went wrong.

The next tests check the species list and a single record. Besides the status code it is worth looking at the response body:

1def test_list_species():
2    response = client.get("/species")
3    assert response.status_code == 200
4    assert isinstance(response.json(), list)
5
6def test_get_species():
7    response = client.get("/species/1")
8    assert response.status_code == 200
9    data = response.json()
10    assert data["id"] == 1
11    assert "name" in data

response.json() turns the response into a Python dict or list. We do not compare the whole record, we only check what really matters: the type of the result, the id and the presence of the name field.

The species creation test sends the request body through the json parameter:

1def test_create_species():
2    response = client.post("/species", json={
3        "name": "Cheetah",
4        "scientific_name": "Acinonyx jubatus",
5        "population": 7100,
6        "habitat": "savanna"
7    })
8    assert response.status_code == 201
9    assert response.json()["name"] == "Cheetah"

Watch out for a trap: by default FastAPI returns 200 for a POST. This test passes only if the endpoint declares @app.post("/species", status_code=201). The 201 Created code describes creating a resource better, so I recommend adding that parameter in the application rather than weakening the test.

The last test checks the error path:

1def test_species_not_found():
2    response = client.get("/species/999")
3    assert response.status_code == 404

Testing negative cases matters as much as testing positive ones. A species with id 999 does not exist, so we expect 404, not 500 or an empty response.

Run the tests:

A single command in the project folder is enough:

1pytest

Every dot in the output is a test that passed, and the letter F marks a failure with a detailed description below. The -v flag prints the name of each test.

Async tests

Sometimes the test itself has to be asynchronous, for example when it calls async database functions. Then instead of TestClient we use AsyncClient from httpx, and the @pytest.mark.asyncio decorator from the pytest-asyncio package tells pytest that the function must run in an event loop:

1import pytest
2from httpx import ASGITransport, AsyncClient
3from main import app
4
5@pytest.mark.asyncio
6async def test_list_species_async():
7    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
8        response = await client.get("/species")
9        assert response.status_code == 200

ASGITransport connects the client straight to the application, without a network, and base_url can be anything, because no request leaves the process. In older code, including the exercises, you will come across the shortcut AsyncClient(app=app). httpx deprecated it in version 0.27 and removed it in version 0.28 - today it ends in a TypeError. The FastAPI documentation uses the @pytest.mark.anyio decorator instead of pytest-asyncio - both approaches work, pick one and stick to it across the whole project.

Good habits

The tests above assume that the database contains a species with id 1. That is a fragile assumption: the result depends on whatever happens to be in the database. In practice tests use a separate test database, and FastAPI lets you swap the get_db dependency through app.dependency_overrides, so the endpoints get a test session without any change to their code. Write tests together with the feature, not "some day" - the longer you wait, the harder they are to add.

Next lesson: Production deployment! Green tests are the pass without which an application should not head off to the cloud.

Remember: a test is the gear check before departure - ten minutes in camp saves hours on the savanna with a broken radio.

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is pytest-asyncio used for?

  2. 2. What does TestClient provide in FastAPI?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Vertical ordering

    Arrange an asynchronous endpoint test:

  • Click in order

    Arrange the assertion checking the status code:

  • Code editor

    Create an async test that checks that GET /species returns status 200 and a list.

Useful articles