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!
Top comments (0)