Test
Project setup
First, create a new project and install the required testing dependencies:
uv init test
cd test
uv add fastapi[standard]
uv add --dev pytest httpxFastAPI testing is built on HTTPX, which shares a similar design with the popular Requests library. This makes testing familiar and intuitive.
Since HTTPX is compatible with PyTest, you can use pytest directly to test your FastAPI applications.
TestClient
Create your FastAPI application in main.py:
=
return Then create a test file test_main.py:
=
=
assert == 200
assert == Writing Tests
- Create a TestClient instance by passing your FastAPI application to it
- Define test functions with names starting with
test_(standardpytestconvention) - Make HTTP requests using the
TestClientobject the same way as you would withhttpx - Add assertions using standard Python
assertstatements to verify the expected behavior
Notice that test functions use regular def, not async def.
Client calls are also synchronous (no await needed).
This allows you to use pytest directly without complications.
Extended example
Let’s extend this example with more details to demonstrate testing different scenarios.
Suppose your main.py file now includes additional path operations:
- A
GEToperation that may return errors - A
POSToperation that may return multiple types of errors - Both operations require an
X-Tokenheader for authentication
=
:
:
: | None = None
: = 1: ,
2: ,
3: ,
}
=
return
=
return Here are comprehensive tests covering both success and error cases:
=
=
assert == 200
assert ==
=
assert == 400
assert ==
=
assert == 404
assert ==
= ,
=,
=,
)
assert == 200
assert ==
= ,
=,
=
)
assert == 400
assert ==
= ,
=,
=,
)
assert == 409
assert == Passing data to the TestClient
When you need to send data in requests, and you’re unsure of the syntax, you can reference the HTTPX documentation or even search for examples using the Requests library, since HTTPX’s API is based on Requests.
Common patterns:
| Data Type | How to Pass It |
|---|---|
| Path or query parameters | Add them directly to the URL |
| JSON body | Pass a Python object (e.g., a dict) to the json parameter |
| Form data | Use the data parameter instead of json |
| Headers | Pass a dict to the headers parameter |
| Cookies | Pass a dict to the cookies parameter |
Note that the TestClient receives data that can be converted to JSON, not Pydantic models.
If you have a Pydantic model in your test and you want to send its data to the application during testing, you can use the jsonable_encoder described in JSON Compatible Encoder.
Async tests
Why async tests?
Asynchronous tests are essential when your application interacts with async components like databases, external APIs, or message queues. For example, if you’re using an async database library, you’ll want to verify that your FastAPI endpoints correctly write data to the database.
However, this introduces a challenge: how do you test async operations effectively?
Installing dependencies
First, add the required testing dependency for async support:
uv add --dev trioUsing pytest.mark.anyio
To test asynchronous code, your test functions must also be asynchronous. The AnyIO library provides a pytest plugin that enables async test execution.
Mark your test functions with @pytest.mark.anyio to run them asynchronously.
Why you can’t use TestClient for async tests
Even if your FastAPI application uses regular def functions instead of async def, FastAPI is fundamentally asynchronous under the hood.
The TestClient uses internal mechanisms to call your asynchronous FastAPI application from synchronous test functions. However, this approach breaks down when your test function itself is asynchronous. When running tests with async def, you cannot use TestClient.
Instead, you’ll use HTTPX’s AsyncClient directly.
Using AsyncClient
Since TestClient is built on top of HTTPX, you can use HTTPX’s AsyncClient to test your API directly in async tests.
Update your test_main.py file:
import pytest
from httpx import ASGITransport, AsyncClient
@pytest.mark.anyio
async def test_get_async():
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as async_client:
response = await async_client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}Key differences from synchronous tests:
- Test function declaration: Use
async definstead ofdef - Decorator: Add
@pytest.mark.anyiobefore the test function - Client: Use
AsyncClientinstead ofTestClient - Requests: Use
awaitwhen making requests - Context manager: Use
async withfor proper resource management
The AsyncClient doesn’t automatically trigger lifespan events (startup/shutdown). If your application depends on these events, use LifespanManager from florimondmanca/asgi-lifespan.
Testing with other async operations
Now that your test function is asynchronous, you can freely await other async operations within your tests. This is particularly useful for:
- Querying async databases
- Making external API calls
- Verifying background tasks
- Checking message queues
For example, you might test an endpoint and then verify the database state:
= await
# Now you can await async database operations
= await # Async database call
assert == If you encounter a RuntimeError: Task attached to a different loop when integrating asynchronous function calls in your tests (e.g. when using MongoDB’s MotorClient), remember to instantiate objects that need an event loop only within async functions, e.g. an @app.on_event("startup") callback.