Python · Testing with pytest · Expert
Testing FastAPI Apps
Quick topic start and explanations before exercises (exercises below):
FastAPI Testing Patterns in Practice
#FastAPI Testing Reference
#Exercises:
Test a FastAPI GET endpoint with TestClient
#You have a FastAPI app with `GET /items/` that returns `{'results': [...]}`. Write a test using `TestClient` that: 1. Sends `GET /items/` 2. Asserts status 200 3. Asserts the response JSON has a `results` key 4. Asserts `results` is a list
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
items = [{'name': 'Widget', 'price': 9.99}]
@app.get('/items/')
def list_items():
return {'results': items}
def test_list_items():
pass
Solution
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
items = [{'name': 'Widget', 'price': 9.99}]
@app.get('/items/')
def list_items():
return {'results': items}
client = TestClient(app)
def test_list_items():
response = client.get('/items/')
assert response.status_code == 200
data = response.json()
assert 'results' in data
assert isinstance(data['results'], list)
Test a POST endpoint and assert the response body
#You have `POST /items/` that accepts `{'name': str, 'price': float}` as a JSON body, creates the item, and returns it with HTTP 201. Write two tests: 1. **test_create_item** — post `{'name': 'Gadget', 'price': 24.99}` and assert: status 201, `name == 'Gadget'`, `price == 24.99`. 2. **test_create_item_missing_field** — post without `price` and assert status 422 (FastAPI's Pydantic validation error).
from fastapi import FastAPI
from pydantic import BaseModel
from fastapi.testclient import TestClient
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.post('/items/', status_code=201)
def create_item(item: Item):
return item.model_dump()
client = TestClient(app)
def test_create_item():
pass
def test_create_item_missing_field():
pass
Solution
from fastapi import FastAPI
from pydantic import BaseModel
from fastapi.testclient import TestClient
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.post('/items/', status_code=201)
def create_item(item: Item):
return item.model_dump()
client = TestClient(app)
def test_create_item():
response = client.post('/items/', json={'name': 'Gadget', 'price': 24.99})
assert response.status_code == 201
data = response.json()
assert data['name'] == 'Gadget'
assert data['price'] == 24.99
def test_create_item_missing_field():
response = client.post('/items/', json={'name': 'Gadget'})
assert response.status_code == 422
Use dependency_overrides to isolate test state
#You have a FastAPI app where `GET /items/` and `POST /items/` share a module-level list through a `get_store` dependency. Tests pollute each other because they all write to the same list. Write a pytest fixture `isolated_client` that: 1. Creates a fresh `[]` for each test. 2. Sets `app.dependency_overrides[get_store]` to a function returning that list. 3. Yields a `TestClient`. 4. Clears `app.dependency_overrides` after the test. Write two tests using this fixture that prove isolation — neither test sees the other's data.
from fastapi import FastAPI, Depends
from pydantic import BaseModel
from fastapi.testclient import TestClient
import pytest
app = FastAPI()
shared_store: list = []
def get_store():
return shared_store
class Item(BaseModel):
name: str
@app.get('/items/')
def list_items(store: list = Depends(get_store)):
return {'results': store}
@app.post('/items/', status_code=201)
def create_item(item: Item, store: list = Depends(get_store)):
record = item.model_dump()
store.append(record)
return record
@pytest.fixture
def isolated_client():
pass
def test_create_item(isolated_client):
pass
def test_store_starts_empty(isolated_client):
pass
Solution
from fastapi import FastAPI, Depends
from pydantic import BaseModel
from fastapi.testclient import TestClient
import pytest
app = FastAPI()
shared_store: list = []
def get_store():
return shared_store
class Item(BaseModel):
name: str
@app.get('/items/')
def list_items(store: list = Depends(get_store)):
return {'results': store}
@app.post('/items/', status_code=201)
def create_item(item: Item, store: list = Depends(get_store)):
record = item.model_dump()
store.append(record)
return record
@pytest.fixture
def isolated_client():
store = []
app.dependency_overrides[get_store] = lambda: store
yield TestClient(app)
app.dependency_overrides.clear()
def test_create_item(isolated_client):
isolated_client.post('/items/', json={'name': 'Widget'})
response = isolated_client.get('/items/')
assert len(response.json()['results']) == 1
assert response.json()['results'][0]['name'] == 'Widget'
def test_store_starts_empty(isolated_client):
response = isolated_client.get('/items/')
assert response.json()['results'] == []
Write an async test with httpx.AsyncClient
#You have a FastAPI app with `GET /items/` that returns `{'results': [...]}`. Set `asyncio_mode = auto` in `pytest.ini`. Write: 1. An `async` pytest fixture `async_client` that creates `httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url='http://test')` using `async with ... yield`. 2. An async test `test_list_items_async` that awaits `GET /items/` and asserts status 200 and a `results` key in the response.
# pytest.ini:
# [pytest]
# asyncio_mode = auto
import httpx
import pytest
from fastapi import FastAPI
app = FastAPI()
items = [{'name': 'Widget'}]
@app.get('/items/')
def list_items():
return {'results': items}
@pytest.fixture
async def async_client():
pass
async def test_list_items_async(async_client):
pass
Solution
# pytest.ini:
# [pytest]
# asyncio_mode = auto
import httpx
import pytest
from fastapi import FastAPI
app = FastAPI()
items = [{'name': 'Widget'}]
@app.get('/items/')
def list_items():
return {'results': items}
@pytest.fixture
async def async_client():
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url='http://test') as client:
yield client
async def test_list_items_async(async_client):
response = await async_client.get('/items/')
assert response.status_code == 200
assert 'results' in response.json()
Test a protected endpoint with an auth token fixture
#You have a FastAPI app with: - `POST /token` — accepts OAuth2 form data, returns `{'access_token': ..., 'token_type': 'bearer'}` - `GET /me` — requires a valid Bearer token, returns `{'username': ...}`; raises 401 without one Write a pytest fixture `auth_client` that: 1. Creates a `TestClient`. 2. Posts credentials to `/token` using `data={}` (form-encoded) and extracts the token. 3. Sets `Authorization: Bearer <token>` on the client for all subsequent requests. 4. Yields the authenticated client. Write: - `test_get_profile_authenticated` — asserts 200 and `{'username': 'alice'}`. - `test_get_profile_no_token` — uses a plain `TestClient`, asserts 401.
import pytest
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.testclient import TestClient
app = FastAPI()
users = {'alice': 'secret'}
tokens = {}
oauth2 = OAuth2PasswordBearer(tokenUrl='/token')
@app.post('/token')
def login(form: OAuth2PasswordRequestForm = Depends()):
if users.get(form.username) != form.password:
raise HTTPException(status_code=400, detail='Bad credentials')
token = f'tok-{form.username}'
tokens[token] = form.username
return {'access_token': token, 'token_type': 'bearer'}
def get_user(token: str = Depends(oauth2)):
name = tokens.get(token)
if not name:
raise HTTPException(status_code=401)
return name
@app.get('/me')
def me(username: str = Depends(get_user)):
return {'username': username}
@pytest.fixture
def auth_client():
pass
def test_get_profile_authenticated(auth_client):
pass
def test_get_profile_no_token():
pass
Solution
import pytest
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.testclient import TestClient
app = FastAPI()
users = {'alice': 'secret'}
tokens = {}
oauth2 = OAuth2PasswordBearer(tokenUrl='/token')
@app.post('/token')
def login(form: OAuth2PasswordRequestForm = Depends()):
if users.get(form.username) != form.password:
raise HTTPException(status_code=400, detail='Bad credentials')
token = f'tok-{form.username}'
tokens[token] = form.username
return {'access_token': token, 'token_type': 'bearer'}
def get_user(token: str = Depends(oauth2)):
name = tokens.get(token)
if not name:
raise HTTPException(status_code=401)
return name
@app.get('/me')
def me(username: str = Depends(get_user)):
return {'username': username}
@pytest.fixture
def auth_client():
client = TestClient(app)
resp = client.post(
'/token',
data={'username': 'alice', 'password': 'secret'},
)
token = resp.json()['access_token']
client.headers.update({'Authorization': f'Bearer {token}'})
yield client
tokens.clear()
def test_get_profile_authenticated(auth_client):
response = auth_client.get('/me')
assert response.status_code == 200
assert response.json() == {'username': 'alice'}
def test_get_profile_no_token():
client = TestClient(app)
response = client.get('/me')
assert response.status_code == 401