How do you know your API still works after changing the code? Automated tests help catch bugs before they reach production.
API tests verify the request flow, including validation, business logic, database operations, and HTTP responses.
Request → Validation → Business Logic → Database → Response
pip install pytest pytest-asyncio httpx
Suppose your FastAPI application has a health endpoint:
from fastapi import FastAPIapp = FastAPI()@app.get("/health")async def health():return {"status": "ok"}
Create tests/test_health.py:
import pytestfrom httpx import ASGITransport, AsyncClientfrom app.main import app@pytest.mark.asyncioasync def test_health():transport = ASGITransport(app=app)async with AsyncClient(transport=transport,base_url="http://test",) as client:response = await client.get("/health")assert response.status_code == 200assert response.json() == {"status": "ok"}
ASGITransport sends requests directly to the ASGI application, allowing you to test the request/response flow without launching Uvicorn.
Run the tests:
pytest -v
Fixtures provide reusable test resources, such as an HTTP client, database session, or authenticated test user.
Create tests/conftest.py:
import pytestfrom httpx import ASGITransport, AsyncClientfrom app.main import app@pytest.fixturedef anyio_backend():return "asyncio"@pytest.fixtureasync def client():transport = ASGITransport(app=app)async with AsyncClient(transport=transport,base_url="http://test",) as test_client:yield test_client
Then write a shorter test:
import pytest@pytest.mark.anyioasync def test_health(client):response = await client.get("/health")assert response.status_code == 200assert response.json() == {"status": "ok"}
Pytest automatically discovers fixtures in conftest.py, so multiple test files can reuse the same setup.
For this example, use AnyIO’s pytest.mark.anyio consistently with the configured backend. Alternatively, use pytest-asyncio and its pytest.mark.asyncio marker throughout the project.
Never run automated tests against your production database. Use a dedicated test database to prevent test operations from modifying real data.
FastAPI lets you replace dependencies through app.dependency_overrides.
For example:
app.dependency_overrides[get_db] = override_get_test_db
Here, override_get_test_db should yield sessions connected to your test database.
Clean up overrides after testing:
app.dependency_overrides.clear()
For reliable database tests, create the required schema before tests run and roll back transactions or clean up test data afterward.
A good test suite verifies both successful requests and expected errors.
| Scenario | Expected status |
|---|---|
| Create a plant successfully | 201 |
| Retrieve an existing plant | 200 |
| Request a missing plant | 404 |
| Submit invalid request data | 422 |
| Access a protected endpoint without authentication | 401 |
| Create a duplicate resource | 409, if defined by the API |
Example: testing invalid input.
@pytest.mark.anyioasync def test_create_plant_with_invalid_price(client):response = await client.post("/plants",json={"name": "Monstera", "price": -10},)assert response.status_code == 422
This test assumes the endpoint validates that the price must be positive.
Don’t check only the status code. Also verify the response body, database changes, and relevant side effects.
As your application grows, separate tests by responsibility.
tests/├── conftest.py├── unit/│ └── test_plant_service.py├── integration/│ └── test_plant_api.py└── e2e/└── test_plant_workflow.py
ASGITransport to test FastAPI without a live server.Automated testing gives you confidence that code changes haven’t broken existing functionality and helps catch regressions before deployment.