DEV Community

Dhaarani Dhanasekeran
Dhaarani Dhanasekeran

Posted on

How to Test FastAPI Endpoints

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!'}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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' }
Enter fullscreen mode Exit fullscreen mode

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)