Introduction

FastAPI is a modern Python web framework for building APIs. Testing FastAPI endpoints is crucial for ensuring that your application behaves as expected. This article will cover the basics of testing FastAPI endpoints, including how to set up tests, common testing techniques, and common pitfalls to avoid.

Setting Up Tests

Before we dive into testing, make sure your FastAPI application has a testing setup. Typically, you'll need to run your FastAPI application in a development server (e.g., uvicorn) and use a testing framework like pytest or unittest.

Testing Basics

Request and Response Assertions

The most basic tests involve making requests to your API and asserting that the response matches your expectations. Here's an example using pytest and uvicorn:

# test_app.py from fastapi import FastAPI import pytest app = FastAPI() @app.get('/hello') async def hello_world() -> dict: return {'message': 'Hello, world!'} # tests/test_app.py from app import app from fastapi.testclient import TestClient client = TestClient(app) @pytest.mark.parametrize('method, path, status_code', [ ('GET', '/hello', 200) ]) def test_hello_world(method, path, status_code): response = client.request(method, path) assert response.status_code == status_code assert response.json() == {'message': 'Hello, world!'} 

Request Parameters and Headers

You can also test parameters and headers passed to your endpoints. For example, let's test a route that accepts a query parameter:

# test_app.py from fastapi import FastAPI import pytest app = FastAPI() @app.get('/items', response_model=list[dict]) async def get_items(q: str | None = None, skip: int = 0, limit: int = 10): return [{'item_name': f'item-{i}'} for i in range(skip, skip + limit)] # tests/test_app.py from app import app from fastapi.testclient import TestClient client = TestClient(app) @pytest.mark.parametrize('method, path, q, skip, limit, status_code, response_data', [ ('GET', '/items?q=foo', 'foo', 0, 10, 200, [{'item_name': 'item-0'}, {'item_name': 'item-10'}]), ('GET', '/items?q=foo&skip=1', 'foo', 1, 10, 200, [{'item_name': 'item-11'}, {'item_name': 'item-20'}]), ('GET', '/items?q=foo&skip=2', 'foo', 2, 10, 200, [{'item_name': 'item-12'}, {'item_name': 'item-21'}]), ]) def test_items(method, path, q, skip, limit, status_code, response_data): response = client.request(method, path, params={'q': q, 'skip': skip, 'limit': limit}) assert response.status_code == status_code assert response.json() == response_data 

Common Pitfalls

Mocking Dependencies

Often, your FastAPI app depends on external services or databases. When testing, you'll want to mock these dependencies to avoid making real calls. For example, if your app fetches data from a database, you can use a library like pytest-mock to mock the database calls.

# tests/test_app.py from fastapi import FastAPI from fastapi.testclient import TestClient from pytest_mock import MockFixture app = FastAPI() def _dummy_db(): return DummyDB() class DummyDB: def fetch_data(self, id: int) -> dict: return {'id': id, 'name': f'item-{id}'} app.dependency_overrides[_dummy_db] = _dummy_db client = TestClient(app) @pytest.mark.parametrize('method, path, id, status_code, response_data', [ ('GET', '/items/{id}', 1, 200, {'id': 1, 'name': 'item-1'}), ('GET', '/items/{id}', 2, 200, {'id': 2, 'name': 'item-2'}), ]) def test_items(method, path, id, status_code, response_data): response = client.request(method, path, params={'id': id}) assert response.status_code == status_code assert response.json() == response_data 

Handling HTTP Errors

Sometimes, you'll want to test error scenarios, such as when a required field is missing. You can mock HTTP errors using pytest's pytest.raises fixture.

# tests/test_app.py from fastapi import FastAPI, HTTPException from fastapi.testclient import TestClient app = FastAPI() class DummyDB: def fetch_data(self, id: int) -> dict: return {'id': id, 'name': f'item-{id}'} app.dependency_overrides[_dummy_db] = _dummy_db client = TestClient(app) @app.get('/items/{id}', response_model=dict) async def get_item(id: int): return await _dummy_db.fetch_data(id) # tests/test_app.py from app import app @pytest.mark.parametrize('method, path, id, status_code', [ ('GET', '/items/100', 100, 400) ]) def test_get_item_with_invalid_id(method, path, id, status_code): response = client.request(method, path, params={'id': id}) assert response.status_code == status_code assert response.json() == {'detail': 'Invalid id: 100' } 

Conclusion

Testing FastAPI endpoints is an essential part of building reliable and maintainable applications. By setting up proper tests, you can ensure that your API behaves as expected. Remember to mock dependencies and handle error scenarios to get the most out of your tests. Happy testing!