FastAPI Unit Testing with Pytest Writing Unit Tests

Learning objective: By the end of this lesson, students will be able to write unit tests for FastAPI endpoints using Pytest.

Fixtures

Pytest provides a feature called fixtures, which allows us to run setup and cleanup code before and after tests. This helps keep tests efficient and independent.

In our conftest.py file, we define fixtures to seed the test database with sample users and teas before each test runs and then clear the data afterward. Without fixtures, we would need to set up test data inside every test function, which would be repetitive and slow.

Fixtures ensure that each test starts with a fresh database state, preventing one test from affecting another. This helps maintain test reliability.

How pytest fixtures work

A fixture uses the yield statement to control execution flow:

  1. Everything before yield runs before the test.
  2. The test function runs.
  3. Everything after yield runs after the test, cleaning up any test data.

By using fixtures, we know that the database is reset after each test, keeping our tests isolated.

Creating unit tests

Now that we have our test setup ready, let’s write actual unit tests.

Testing GET all teas

Before we begin writing the test, let’s create the file where we’ll store our test for fetching all teas from the database.

touch tests/test_teas.py

Now, let’s proceed with writing the test for GET all teas.

# tests/test_teas.py

import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from models.user import UserModel
from models.tea import TeaModel
from tests.lib import login
from main import app

def test_get_teas(test_app: TestClient, override_get_db):
    response = test_app.get("/api/teas")
    assert response.status_code == 200
    teas = response.json()
    assert isinstance(teas, list)
    assert len(teas) >= 2  # Ensure there are at least two teas in the test database
    for tea in teas:
        assert 'id' in tea
        assert 'name' in tea
        assert 'in_stock' in tea
        assert 'rating' in tea
        assert 'user' in tea
        assert 'email' in tea['user']
        assert 'username' in tea['user']

Key points in this test:

To run this test, use the command:

pipenv run pytest

If successful, Pytest will display a green success message. If it fails, Pytest will show which assertion failed.

Testing POST create tea

Next, let’s write a test to create a new tea. Since authentication is required, we must first log in and obtain a token.

# tests/test_teas.py


def test_create_tea(test_app: TestClient, test_db: Session):

    # Create a new mock user in the test database
    user = UserModel(username='testUser123', email='hello@example.com')
    user.set_password('mys3cretp2ssw0rd')
    test_db.add(user)
    test_db.commit()

    # Use the login helper to generate authentication headers for the new mock user
    headers = login(test_app, 'testUser123', 'mys3cretp2ssw0rd')

    # Data for creating a new tea
    tea_data = {
        "name": "Test Tea",
        "in_stock": True,
        "rating": 4
    }

    # Send a POST request to create a new tea
    response = test_app.post("/api/teas", headers=headers, json=tea_data)

    # Verify that the response is successful
    assert response.status_code == 200
    assert response.json()["name"] == tea_data["name"]
    assert response.json()["in_stock"] == tea_data["in_stock"]
    assert response.json()["rating"] == tea_data["rating"]
    assert "id" in response.json()  # Ensure an ID is returned
    assert "user" in response.json()  # Ensure user data is included
    assert response.json()['user']["username"] == 'testUser123'

    # Verify the tea was created in the database
    tea_id = response.json()["id"]
    tea = test_db.query(TeaModel).filter(TeaModel.id == tea_id).first()
    assert tea is not None
    assert tea.name == tea_data["name"]
    assert tea.in_stock == tea_data["in_stock"]
    assert tea.rating == tea_data["rating"]

This test does the following:

Run the test again with:

pipenv run pytest

If everything is set up correctly, this test should pass as well.

With these tests in place, we can confidently verify that our GET and POST endpoints behave as expected!