FastAPI TestClient: Debugging and Inspecting Test Output

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.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *