Movatterモバイル変換


[0]ホーム

URL:


Skip to content
Join theFastAPI Cloud waiting list 🚀
Follow@fastapi onX (Twitter) to stay updated
FollowFastAPI onLinkedIn to stay updated
Subscribe to theFastAPI and friends newsletter 🎉
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor

Testing

Thanks toStarlette, testingFastAPI applications is easy and enjoyable.

It is based onHTTPX, which in turn is designed based on Requests, so it's very familiar and intuitive.

With it, you can usepytest directly withFastAPI.

UsingTestClient

Info

To useTestClient, first installhttpx.

Make sure you create avirtual environment, activate it, and then install it, for example:

$pipinstallhttpx

ImportTestClient.

Create aTestClient by passing yourFastAPI application to it.

Create functions with a name that starts withtest_ (this is standardpytest conventions).

Use theTestClient object the same way as you do withhttpx.

Write simpleassert statements with the standard Python expressions that you need to check (again, standardpytest).

fromfastapiimportFastAPIfromfastapi.testclientimportTestClientapp=FastAPI()@app.get("/")asyncdefread_main():return{"msg":"Hello World"}client=TestClient(app)deftest_read_main():response=client.get("/")assertresponse.status_code==200assertresponse.json()=={"msg":"Hello World"}

Tip

Notice that the testing functions are normaldef, notasync def.

And the calls to the client are also normal calls, not usingawait.

This allows you to usepytest directly without complications.

Technical Details

You could also usefrom starlette.testclient import TestClient.

FastAPI provides the samestarlette.testclient asfastapi.testclient just as a convenience for you, the developer. But it comes directly from Starlette.

Tip

If you want to callasync functions in your tests apart from sending requests to your FastAPI application (e.g. asynchronous database functions), have a look at theAsync Tests in the advanced tutorial.

Separating tests

In a real application, you probably would have your tests in a different file.

And yourFastAPI application might also be composed of several files/modules, etc.

FastAPI app file

Let's say you have a file structure as described inBigger Applications:

.├── app│   ├── __init__.py│   └── main.py

In the filemain.py you have yourFastAPI app:

fromfastapiimportFastAPIapp=FastAPI()@app.get("/")asyncdefread_main():return{"msg":"Hello World"}

Testing file

Then you could have a filetest_main.py with your tests. It could live on the same Python package (the same directory with a__init__.py file):

.├── app│   ├── __init__.py│   ├── main.py│   └── test_main.py

Because this file is in the same package, you can use relative imports to import the objectapp from themain module (main.py):

fromfastapi.testclientimportTestClientfrom.mainimportappclient=TestClient(app)deftest_read_main():response=client.get("/")assertresponse.status_code==200assertresponse.json()=={"msg":"Hello World"}

...and have the code for the tests just like before.

Testing: extended example

Now let's extend this example and add more details to see how to test different parts.

ExtendedFastAPI app file

Let's continue with the same file structure as before:

.├── app│   ├── __init__.py│   ├── main.py│   └── test_main.py

Let's say that now the filemain.py with yourFastAPI app has some otherpath operations.

It has aGET operation that could return an error.

It has aPOST operation that could return several errors.

Bothpath operations require anX-Token header.

fromtypingimportAnnotatedfromfastapiimportFastAPI,Header,HTTPExceptionfrompydanticimportBaseModelfake_secret_token="coneofsilence"fake_db={"foo":{"id":"foo","title":"Foo","description":"There goes my hero"},"bar":{"id":"bar","title":"Bar","description":"The bartenders"},}app=FastAPI()classItem(BaseModel):id:strtitle:strdescription:str|None=None@app.get("/items/{item_id}",response_model=Item)asyncdefread_main(item_id:str,x_token:Annotated[str,Header()]):ifx_token!=fake_secret_token:raiseHTTPException(status_code=400,detail="Invalid X-Token header")ifitem_idnotinfake_db:raiseHTTPException(status_code=404,detail="Item not found")returnfake_db[item_id]@app.post("/items/")asyncdefcreate_item(item:Item,x_token:Annotated[str,Header()])->Item:ifx_token!=fake_secret_token:raiseHTTPException(status_code=400,detail="Invalid X-Token header")ifitem.idinfake_db:raiseHTTPException(status_code=409,detail="Item already exists")fake_db[item.id]=item.model_dump()returnitem
🤓 Other versions and variants

Tip

Prefer to use theAnnotated version if possible.

fromfastapiimportFastAPI,Header,HTTPExceptionfrompydanticimportBaseModelfake_secret_token="coneofsilence"fake_db={"foo":{"id":"foo","title":"Foo","description":"There goes my hero"},"bar":{"id":"bar","title":"Bar","description":"The bartenders"},}app=FastAPI()classItem(BaseModel):id:strtitle:strdescription:str|None=None@app.get("/items/{item_id}",response_model=Item)asyncdefread_main(item_id:str,x_token:str=Header()):ifx_token!=fake_secret_token:raiseHTTPException(status_code=400,detail="Invalid X-Token header")ifitem_idnotinfake_db:raiseHTTPException(status_code=404,detail="Item not found")returnfake_db[item_id]@app.post("/items/")asyncdefcreate_item(item:Item,x_token:str=Header())->Item:ifx_token!=fake_secret_token:raiseHTTPException(status_code=400,detail="Invalid X-Token header")ifitem.idinfake_db:raiseHTTPException(status_code=409,detail="Item already exists")fake_db[item.id]=item.model_dump()returnitem

Extended testing file

You could then updatetest_main.py with the extended tests:

fromfastapi.testclientimportTestClientfrom.mainimportappclient=TestClient(app)deftest_read_item():response=client.get("/items/foo",headers={"X-Token":"coneofsilence"})assertresponse.status_code==200assertresponse.json()=={"id":"foo","title":"Foo","description":"There goes my hero",}deftest_read_item_bad_token():response=client.get("/items/foo",headers={"X-Token":"hailhydra"})assertresponse.status_code==400assertresponse.json()=={"detail":"Invalid X-Token header"}deftest_read_nonexistent_item():response=client.get("/items/baz",headers={"X-Token":"coneofsilence"})assertresponse.status_code==404assertresponse.json()=={"detail":"Item not found"}deftest_create_item():response=client.post("/items/",headers={"X-Token":"coneofsilence"},json={"id":"foobar","title":"Foo Bar","description":"The Foo Barters"},)assertresponse.status_code==200assertresponse.json()=={"id":"foobar","title":"Foo Bar","description":"The Foo Barters",}deftest_create_item_bad_token():response=client.post("/items/",headers={"X-Token":"hailhydra"},json={"id":"bazz","title":"Bazz","description":"Drop the bazz"},)assertresponse.status_code==400assertresponse.json()=={"detail":"Invalid X-Token header"}deftest_create_existing_item():response=client.post("/items/",headers={"X-Token":"coneofsilence"},json={"id":"foo","title":"The Foo ID Stealers","description":"There goes my stealer",},)assertresponse.status_code==409assertresponse.json()=={"detail":"Item already exists"}
🤓 Other versions and variants

Tip

Prefer to use theAnnotated version if possible.

fromfastapi.testclientimportTestClientfrom.mainimportappclient=TestClient(app)deftest_read_item():response=client.get("/items/foo",headers={"X-Token":"coneofsilence"})assertresponse.status_code==200assertresponse.json()=={"id":"foo","title":"Foo","description":"There goes my hero",}deftest_read_item_bad_token():response=client.get("/items/foo",headers={"X-Token":"hailhydra"})assertresponse.status_code==400assertresponse.json()=={"detail":"Invalid X-Token header"}deftest_read_nonexistent_item():response=client.get("/items/baz",headers={"X-Token":"coneofsilence"})assertresponse.status_code==404assertresponse.json()=={"detail":"Item not found"}deftest_create_item():response=client.post("/items/",headers={"X-Token":"coneofsilence"},json={"id":"foobar","title":"Foo Bar","description":"The Foo Barters"},)assertresponse.status_code==200assertresponse.json()=={"id":"foobar","title":"Foo Bar","description":"The Foo Barters",}deftest_create_item_bad_token():response=client.post("/items/",headers={"X-Token":"hailhydra"},json={"id":"bazz","title":"Bazz","description":"Drop the bazz"},)assertresponse.status_code==400assertresponse.json()=={"detail":"Invalid X-Token header"}deftest_create_existing_item():response=client.post("/items/",headers={"X-Token":"coneofsilence"},json={"id":"foo","title":"The Foo ID Stealers","description":"There goes my stealer",},)assertresponse.status_code==409assertresponse.json()=={"detail":"Item already exists"}

Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it inhttpx, or even how to do it withrequests, as HTTPX's design is based on Requests' design.

Then you just do the same in your tests.

E.g.:

  • To pass apath orquery parameter, add it to the URL itself.
  • To pass a JSON body, pass a Python object (e.g. adict) to the parameterjson.
  • If you need to sendForm Data instead of JSON, use thedata parameter instead.
  • To passheaders, use adict in theheaders parameter.
  • Forcookies, adict in thecookies parameter.

For more information about how to pass data to the backend (usinghttpx or theTestClient) check theHTTPX documentation.

Info

Note that theTestClient 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 thejsonable_encoder described inJSON Compatible Encoder.

Run it

After that, you just need to installpytest.

Make sure you create avirtual environment, activate it, and then install it, for example:

$pipinstallpytest---> 100%

It will detect the files and tests automatically, execute them, and report the results back to you.

Run the tests with:

$pytest================ test session starts ================platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1rootdir: /home/user/code/superawesome-cli/appplugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1collected 6 items---> 100%test_main.py <span style="color: green; white-space: pre;">......                            [100%]</span><span style="color: green;">================= 1 passed in 0.03s =================</span>

[8]ページ先頭

©2009-2026 Movatter.jp