Testing is a crucial aspect of developing robust FastAPI applications. Leveraging the TestClient
, built upon the foundations of Starlette and HTTPX, simplifies this process, making it both efficient and developer-friendly. If you’re encountering issues where your FastAPI TestClient
isn’t showing the output you expect, or you’re seeking best practices for testing, this guide is for you.
Getting Started with TestClient
To begin using TestClient
, ensure you have httpx
installed. If you haven’t already, activate your virtual environment and install it using pip:
<span id="__span-0-1"><span>$ </span>pip install httpx
Once installed, you can import TestClient
from fastapi.testclient
. The core idea is to instantiate TestClient
with your FastAPI application instance. Let’s look at a basic example:
<span id="__span-1-1"><span>from</span><span>fastapi</span><span>import</span> <span>FastAPI</span>
<span id="__span-1-2"><span>from</span><span>fastapi.testclient</span><span>import</span> <span>TestClient</span>
<span id="__span-1-4"><span>app</span> <span>=</span> <span>FastAPI</span><span>()</span>
<span id="__span-1-7"><span>@app</span><span>.</span><span>get</span><span>(</span><span>"/"</span><span>)</span>
<span id="__span-1-8"><span>async</span> <span>def</span><span>read_main</span><span>():</span>
<span id="__span-1-9"> <span>return</span> <span>{</span><span>"msg"</span><span>:</span> <span>"Hello World"</span><span>}</span>
<span id="__span-1-12"><span>client</span> <span>=</span> <span>TestClient</span><span>(</span><span>app</span><span>)</span>
<span id="__span-1-15"><span>def</span><span>test_read_main</span><span>():</span>
<span id="__span-1-16"> <span>response</span> <span>=</span> <span>client</span><span>.</span><span>get</span><span>(</span><span>"/"</span><span>)</span>
<span id="__span-1-17"> <span>assert</span> <span>response</span><span>.</span><span>status_code</span> <span>==</span> <span>200</span>
<span id="__span-1-18"> <span>assert</span> <span>response</span><span>.</span><span>json</span><span>()</span> <span>==</span> <span>{</span><span>"msg"</span><span>:</span> <span>"Hello World"</span><span>}</span>
In this snippet, we first import necessary components, create a simple FastAPI application, and then initialize TestClient
with it. The test_read_main
function demonstrates a basic test case. We use client.get("/")
to simulate a GET request to the root path and then use standard assert
statements to validate the response status code and JSON body.
A key observation is that test functions are defined using def
, not async def
, and client calls are also synchronous, simplifying integration with pytest
.
You might also see from starlette.testclient import TestClient
. FastAPI conveniently re-exports Starlette’s TestClient
as fastapi.testclient
.
For scenarios requiring asynchronous operations within tests (beyond request/response cycles, such as database interactions), refer to the Async Tests section in the advanced documentation.
Structuring Your Tests
In practical projects, separating tests into dedicated files is essential for organization. Furthermore, your FastAPI application might be structured across multiple files or modules.
FastAPI Application File
Consider a project structure like this, as detailed in Bigger Applications:
<span id="__span-2-1">.
<span id="__span-2-2">├── app
<span id="__span-2-3">│ ├── __init__.py
<span id="__span-2-4">│ └── main.py
Your FastAPI
application, defined in main.py
, could look like:
<span id="__span-3-1"><span>from</span><span>fastapi</span><span>import</span> <span>FastAPI</span>
<span id="__span-3-3"><span>app</span> <span>=</span> <span>FastAPI</span><span>()</span>
<span id="__span-3-6"><span>@app</span><span>.</span><span>get</span><span>(</span><span>"/"</span><span>)</span>
<span id="__span-3-7"><span>async</span> <span>def</span><span>read_main</span><span>():</span>
<span id="__span-3-8"> <span>return</span> <span>{</span><span>"msg"</span><span>:</span> <span>"Hello World"</span><span>}</span>
Test File
You can create a test_main.py
file within the same Python package (directory with __init__.py
) to house your tests:
<span id="__span-4-1">.
<span id="__span-4-2">├── app
<span id="__span-4-3">│ ├── __init__.py
<span id="__span-4-4">│ ├── main.py
<span id="__span-4-5"><span>│ └── test_main.py
By placing the test file in the same package, you can use relative imports to access your FastAPI
app
instance from main.py
:
<span id="__span-5-1"><span>from</span><span>fastapi.testclient</span><span>import</span> <span>TestClient</span>
<span id="__span-5-3"><span>from</span><span>.main</span><span>import</span> <span>app</span>
<span id="__span-5-5"><span>client</span> <span>=</span> <span>TestClient</span><span>(</span><span>app</span><span>)</span>
<span id="__span-5-8"><span>def</span><span>test_read_main</span><span>():</span>
<span id="__span-5-9"> <span>response</span> <span>=</span> <span>client</span><span>.</span><span>get</span><span>(</span><span>"/"</span><span>)</span>
<span id="__span-5-10"> <span>assert</span> <span>response</span><span>.</span><span>status_code</span> <span>==</span> <span>200</span>
<span id="__span-5-11"> <span>assert</span> <span>response</span><span>.</span><span>json</span><span>()</span> <span>==</span> <span>{</span><span>"msg"</span><span>:</span> <span>"Hello World"</span><span>}</span>
This setup allows for clean separation of application code and tests while maintaining easy access to application components within your tests.
Advanced Testing Examples and Output Inspection
Let’s expand our example to cover more complex scenarios, including header handling, request body submissions, and error testing. This will also help illustrate how to inspect output when things don’t go as planned.
Extended FastAPI Application
Continuing with the file structure:
<span id="__span-6-1">.
<span id="__span-6-2">├── app
<span id="__span-6-3">│ ├── __init__.py
<span id="__span-6-4">│ ├── main.py
<span id="__span-6-5">│ └── test_main.py
Assume main.py
now includes path operations that require headers and can return errors:
<span id="__span-7-1"><span>from</span><span>typing</span><span>import</span> <span>Annotated</span>
<span id="__span-7-3"><span>from</span><span>fastapi</span><span>import</span> <span>FastAPI</span><span>,</span> <span>Header</span><span>,</span> <span>HTTPException</span>
<span id="__span-7-4"><span>from</span><span>pydantic</span><span>import</span> <span>BaseModel</span>
<span id="__span-7-6"><span>fake_secret_token</span> <span>=</span> <span>"coneofsilence"</span>
<span id="__span-7-8"><span>fake_db</span> <span>=</span> <span>{</span>
<span id="__span-7-9"> <span>"foo"</span><span>:</span> <span>{</span><span>"id"</span><span>:</span> <span>"foo"</span><span>,</span> <span>"title"</span><span>:</span> <span>"Foo"</span><span>,</span> <span>"description"</span><span>:</span> <span>"There goes my hero"</span><span>},</span>
<span id="__span-7-10"> <span>"bar"</span><span>:</span> <span>{</span><span>"id"</span><span>:</span> <span>"bar"</span><span>,</span> <span>"title"</span><span>:</span> <span>"Bar"</span><span>,</span> <span>"description"</span><span>:</span> <span>"The bartenders"</span><span>},</span>
<span id="__span-7-11"><span>}</span>
<span id="__span-7-13"><span>app</span> <span>=</span> <span>FastAPI</span><span>()</span>
<span id="__span-7-16"><span>class</span><span>Item</span><span>(</span><span>BaseModel</span><span>):</span>
<span id="__span-7-17"> <span>id</span><span>:</span> <span>str</span>
<span id="__span-7-18"> <span>title</span><span>:</span> <span>str</span>
<span id="__span-7-19"> <span>description</span><span>:</span> <span>str</span> <span>|</span> <span>None</span> <span>=</span> <span>None</span>
<span id="__span-7-22"><span>@app</span><span>.</span><span>get</span><span>(</span><span>"/items/</span><span>{item_id}</span><span>"</span><span>,</span> <span>response_model</span><span>=</span><span>Item</span><span>)</span>
<span id="__span-7-23"><span>async</span> <span>def</span><span>read_item</span><span>(</span><span>item_id</span><span>:</span> <span>str</span><span>,</span> <span>x_token</span><span>:</span> <span>Annotated</span><span>[</span><span>str</span><span>,</span> <span>Header</span><span>()]):</span>
<span id="__span-7-24"> <span>if</span> <span>x_token</span> <span>!=</span> <span>fake_secret_token</span><span>:</span>
<span id="__span-7-25"> <span>raise</span> <span>HTTPException</span><span>(</span><span>status_code</span><span>=</span><span>400</span><span>,</span> <span>detail</span><span>=</span><span>"Invalid X-Token header"</span><span>)</span>
<span id="__span-7-26"> <span>if</span> <span>item_id</span> <span>not</span> <span>in</span> <span>fake_db</span><span>:</span>
<span id="__span-7-27"> <span>raise</span> <span>HTTPException</span><span>(</span><span>status_code</span><span>=</span><span>404</span><span>,</span> <span>detail</span><span>=</span><span>"Item not found"</span><span>)</span>
<span id="__span-7-28"> <span>return</span> <span>fake_db</span><span>[</span><span>item_id</span><span>]</span>
<span id="__span-7-31"><span>@app</span><span>.</span><span>post</span><span>(</span><span>"/items/"</span><span>,</span> <span>response_model</span><span>=</span><span>Item</span><span>)</span>
<span id="__span-7-32"><span>async</span> <span>def</span><span>create_item</span><span>(</span><span>item</span><span>:</span> <span>Item</span><span>,</span> <span>x_token</span><span>:</span> <span>Annotated</span><span>[</span><span>str</span><span>,</span> <span>Header</span><span>()]):</span>
<span id="__span-7-33"> <span>if</span> <span>x_token</span> <span>!=</span> <span>fake_secret_token</span><span>:</span>
<span id="__span-7-34"> <span>raise</span> <span>HTTPException</span><span>(</span><span>status_code</span><span>=</span><span>400</span><span>,</span> <span>detail</span><span>=</span><span>"Invalid X-Token header"</span><span>)</span>
<span id="__span-7-35"> <span>if</span> <span>item</span><span>.</span><span>id</span> <span>in</span> <span>fake_db</span><span>:</span>
<span id="__span-7-36"> <span>raise</span> <span>HTTPException</span><span>(</span><span>status_code</span><span>=</span><span>409</span><span>,</span> <span>detail</span><span>=</span><span>"Item already exists"</span><span>)</span>
<span id="__span-7-37"> <span>fake_db</span><span>[</span><span>item</span><span>.</span><span>id</span><span>]</span> <span>=</span> <span>item</span>
<span id="__span-7-38"> <span>return</span> <span>item</span>
Extended Test File and Output Troubleshooting
Update test_main.py
to test these new endpoints, paying attention to headers and different response scenarios:
<span id="__span-12-1"><span>from</span><span>fastapi.testclient</span><span>import</span> <span>TestClient</span>
<span id="__span-12-3"><span>from</span><span>.main</span><span>import</span> <span>app</span>
<span id="__span-12-5"><span>client</span> <span>=</span> <span>TestClient</span><span>(</span><span>app</span><span>)</span>
<span id="__span-12-8"><span>def</span><span>test_read_item</span><span>():</span>
<span id="__span-12-9"> <span>response</span> <span>=</span> <span>client</span><span>.</span><span>get</span><span>(</span><span>"/items/foo"</span><span>,</span> <span>headers</span><span>=</span><span>{</span><span>"X-Token"</span><span>:</span> <span>"coneofsilence"</span><span>})</span>
<span id="__span-12-10"> <span>assert</span> <span>response</span><span>.</span><span>status_code</span> <span>==</span> <span>200</span>
<span id="__span-12-11"> <span>assert</span> <span>response</span><span>.</span><span>json</span><span>()</span> <span>==</span> <span>{</span>
<span id="__span-12-12"> <span>"id"</span><span>:</span> <span>"foo"</span><span>,</span>
<span id="__span-12-13"> <span>"title"</span><span>:</span> <span>"Foo"</span><span>,</span>
<span id="__span-12-14"> <span>"description"</span><span>:</span> <span>"There goes my hero"</span><span>,</span>
<span id="__span-12-15"> <span>}</span>
<span id="__span-12-18"><span>def</span><span>test_read_item_bad_token</span><span>():</span>
<span id="__span-12-19"> <span>response</span> <span>=</span> <span>client</span><span>.</span><span>get</span><span>(</span><span>"/items/foo"</span><span>,</span> <span>headers</span><span>=</span><span>{</span><span>"X-Token"</span><span>:</span> <span>"hailhydra"</span><span>})</span>
<span id="__span-12-20"> <span>assert</span> <span>response</span><span>.</span><span>status_code</span> <span>==</span> <span>400</span>
<span id="__span-12-21"> <span>assert</span> <span>response</span><span>.</span><span>json</span><span>()</span> <span>==</span> <span>{</span><span>"detail"</span><span>:</span> <span>"Invalid X-Token header"</span><span>}</span>
<span id="__span-12-24"><span>def</span><span>test_read_nonexistent_item</span><span>():</span>
<span id="__span-12-25"> <span>response</span> <span>=</span> <span>client</span><span>.</span><span>get</span><span>(</span><span>"/items/baz"</span><span>,</span> <span>headers</span><span>=</span><span>{</span><span>"X-Token"</span><span>:</span> <span>"coneofsilence"</span><span>})</span>
<span id="__span-12-26"> <span>assert</span> <span>response</span><span>.</span><span>status_code</span> <span>==</span> <span>404</span>
<span id="__span-12-27"> <span>assert</span> <span>response</span><span>.</span><span>json</span><span>()</span> <span>==</span> <span>{</span><span>"detail"</span><span>:</span> <span>"Item not found"</span><span>}</span>
<span id="__span-12-30"><span>def</span><span>test_create_item</span><span>():</span>
<span id="__span-12-31"> <span>response</span> <span>=</span> <span>client</span><span>.</span><span>post</span><span>(</span>
<span id="__span-12-32"> <span>"/items/"</span><span>,</span>
<span id="__span-12-33"> <span>headers</span><span>=</span><span>{</span><span>"X-Token"</span><span>:</span> <span>"coneofsilence"</span><span>},</span>
<span id="__span-12-34"> <span>json</span><span>=</span><span>{</span><span>"id"</span><span>:</span> <span>"foobar"</span><span>,</span> <span>"title"</span><span>:</span> <span>"Foo Bar"</span><span>,</span> <span>"description"</span><span>:</span> <span>"The Foo Barters"</span><span>},</span>
<span id="__span-12-35"> <span>)</span>
<span id="__span-12-36"> <span>assert</span> <span>response</span><span>.</span><span>status_code</span> <span>==</span> <span>200</span>
<span id="__span-12-37"> <span>assert</span> <span>response</span><span>.</span><span>json</span><span>()</span> <span>==</span> <span>{</span>
<span id="__span-12-38"> <span>"id"</span><span>:</span> <span>"foobar"</span><span>,</span>
<span id="__span-12-39"> <span>"title"</span><span>:</span> <span>"Foo Bar"</span><span>,</span>
<span id="__span-12-40"> <span>"description"</span><span>:</span> <span>"The Foo Barters"</span><span>,</span>
<span id="__span-12-41"> <span>}</span>
<span id="__span-12-44"><span>def</span><span>test_create_item_bad_token</span><span>():</span>
<span id="__span-12-45"> <span>response</span> <span>=</span> <span>client</span><span>.</span><span>post</span><span>(</span>
<span id="__span-12-46"> <span>"/items/"</span><span>,</span>
<span id="__span-12-47"> <span>headers</span><span>=</span><span>{</span><span>"X-Token"</span><span>:</span> <span>"hailhydra"</span><span>},</span>
<span id="__span-12-48"> <span>json</span><span>=</span><span>{</span><span>"id"</span><span>:</span> <span>"bazz"</span><span>,</span> <span>"title"</span><span>:</span> <span>"Bazz"</span><span>,</span> <span>"description"</span><span>:</span> <span>"Drop the bazz"</span><span>},</span>
<span id="__span-12-49"> <span>)</span>
<span id="__span-12-50"> <span>assert</span> <span>response</span><span>.</span><span>status_code</span> <span>==</span> <span>400</span>
<span id="__span-12-51"> <span>assert</span> <span>response</span><span>.</span><span>json</span><span>()</span> <span>==</span> <span>{</span><span>"detail"</span><span>:</span> <span>"Invalid X-Token header"</span><span>}</span>
<span id="__span-12-54"><span>def</span><span>test_create_existing_item</span><span>():</span>
<span id="__span-12-55"> <span>response</span> <span>=</span> <span>client</span><span>.</span><span>post</span><span>(</span>
<span id="__span-12-56"> <span>"/items/"</span><span>,</span>
<span id="__span-12-57"> <span>headers</span><span>=</span><span>{</span><span>"X-Token"</span><span>:</span> <span>"coneofsilence"</span><span>},</span>
<span id="__span-12-58"> <span>json</span><span>=</span><span>{</span>
<span id="__span-12-59"> <span>"id"</span><span>:</span> <span>"foo"</span><span>,</span>
<span id="__span-12-60"> <span>"title"</span><span>:</span> <span>"The Foo ID Stealers"</span><span>,</span>
<span id="__span-12-61"> <span>"description"</span><span>:</span> <span>"There goes my stealer"</span><span>},</span>
<span id="__span-12-62"> <span>}</span>
<span id="__span-12-63"> <span>)</span>
<span id="__span-12-64"> <span>assert</span> <span>response</span><span>.</span><span>status_code</span> <span>==</span> <span>409</span>
<span id="__span-12-65"> <span>assert</span> <span>response</span><span>.</span><span>json</span><span>()</span> <span>==</span> <span>{</span><span>"detail"</span><span>:</span> <span>"Item already exists"</span><span>}</span>
When tests fail or you need to understand the request/response flow in detail, inspecting the response
object becomes crucial. While TestClient
itself might not “print” output in the traditional sense, the response
object holds all the information you need to debug.
For instance, if you’re unsure why a test is failing, you can add print statements to examine the response:
def test_read_item_bad_token():
response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
print("Response Status Code:", response.status_code) # Inspect the status code
print("Response JSON:", response.json()) # Inspect the JSON body
print("Response Text:", response.text) # Inspect the raw text response
print("Response Headers:", response.headers) # Inspect the headers
assert response.status_code == 400
assert response.json() == {"detail": "Invalid X-Token header"}
By printing these attributes of the response
object, you gain clear visibility into what your FastAPI application is returning, helping you pinpoint issues when your TestClient
tests are not behaving as expected or when you are troubleshooting “not printing” scenarios where you need to see more details about the interaction.
Remember to consult the HTTPX documentation for comprehensive details on constructing requests (path parameters, query parameters, JSON bodies, form data, headers, cookies) using TestClient
, as it mirrors HTTPX’s API.
TestClient
works with data convertible to JSON, not directly with Pydantic models. If you need to send Pydantic models in your test requests, utilize jsonable_encoder
as described in JSON Compatible Encoder to convert your models into JSON-compatible data.
Running Your Tests
To execute your tests, you’ll need pytest
. Install it if you haven’t already:
<span id="__span-13-1"><span>$ </span>pip install pytest
pytest
automatically discovers and runs tests in your project. Simply run pytest
in your terminal:
<span id="__span-14-1"><span>$ </span>pytest
<span id="__span-14-3"><span>================ test session starts ================</span>
<span id="__span-14-4"><span>platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1</span>
<span id="__span-14-5"><span>rootdir: /home/user/code/superawesome-cli/app</span>
<span id="__span-14-6"><span>plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1</span>
<span id="__span-14-7"><span>collected 6 items</span>
<span id="__span-14-9"><span>---> 100%</span>
<span id="__span-14-11"><span>test_main.py <span>...... [100%]</span></span>
<span id="__span-14-13"><span><span>================= 1 passed in 0.03s =================</span></span>
pytest
will execute your tests and provide a summary of the results. By strategically using TestClient
and inspecting the response
object, you can effectively test your FastAPI applications and ensure their reliability. Remember to leverage print statements and explore the response
attributes when troubleshooting test output or addressing situations where you need greater visibility into your test interactions.