多亏了Starlette,测试FastAPI应用变得简单而愉快。
它是基于Request的,所以非常熟悉和直观。
有了它,你可以直接用FastAPI使用pytest。

使用TestClient

导入TestClient
创建一个TestClient,将你的FastAPI传递给它。
创建一个以test_ 开头的函数(pytest的标准)

使用TestClient 对象的方法和使用requests一样。

用标准Python表达式写简单的assert语句。

  1. from fastapi import FastAPI
  2. from fastapi.testclient import TestClient
  3. app = FastAPI()
  4. @app.get("/")
  5. async def read_main():
  6. return {"msg": "Hello World"}
  7. client = TestClient(app)
  8. def test_read_main():
  9. response = client.get("/")
  10. assert response.status_code == 200
  11. assert response.json() == {"msg": "Hello World"}

:::info Tip
注意,测试函数是正常的def,而不是async def
对客户端的调用也是正常调用,不使用 await
这让你可以直接使用pytest,而不会出现异常的情况。 :::

:::tips 技术细节
也可以使用from starlette.testclient import TestClient.
FastAPI 提供了相同的方法 fastapi.testclient 仅仅是一个便利,依然来自于Starlette. :::

:::info Tip
如果你想在你的测试中除了向FastAPI应用程序发送请求之外,还想调用异步函数(例如异步数据库函数),请看看高级教程中的异步测试。 :::

分离测试

在真实的应用中,你可能会把你的测试放在不同的文件中。
而你的FastAPI应用也可能由多个文件/模块等组成。

FastAPI应用文件

假设你的FastAPI应用有一个main.py文件

  1. from fastapi import FastAPI
  2. app = FastAPI()
  3. @app.get("/")
  4. async def read_main():
  5. return {"msg": "Hello World"}

测试文件

然后,在你的测试目录中可以有一个test_main.py文件,导入app的main模块(main.py)

  1. from fastapi.testclient import TestClient
  2. from .main import app
  3. client = TestClient(app)
  4. def test_read_main():
  5. response = client.get("/")
  6. assert response.status_code == 200
  7. assert response.json() == {"msg": "Hello World"}

测试:扩展示例

现在让我们扩展这个例子,并添加更多细节,看看如何测试不同的部分。

扩展FastAPI app文件

比方说,你的FastAPI应用有一个main_b.py文件。
有一个GET操作可以返回一个错误。
有一个POST操作可以返回一个内部错误。
这两种路径操作都需要一个X-Token头。

  1. from typing import Optional
  2. from fastapi import FastAPI, Header, HTTPException
  3. from pydantic import BaseModel
  4. fake_secret_token = "coneofsilence"
  5. fake_db = {
  6. "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
  7. "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
  8. }
  9. app = FastAPI()
  10. class Item(BaseModel):
  11. id: str
  12. title: str
  13. description: Optional[str] = None
  14. @app.get("/items/{item_id}", response_model=Item)
  15. async def read_main(item_id: str, x_token: str = Header(...)):
  16. if x_token != fake_secret_token:
  17. raise HTTPException(status_code=400, detail="Invalid X-Token header")
  18. if item_id not in fake_db:
  19. raise HTTPException(status_code=404, detail="Item not found")
  20. return fake_db[item_id]
  21. @app.post("/items/", response_model=Item)
  22. async def create_item(item: Item, x_token: str = Header(...)):
  23. if x_token != fake_secret_token:
  24. raise HTTPException(status_code=400, detail="Invalid X-Token header")
  25. if item.id in fake_db:
  26. raise HTTPException(status_code=400, detail="Item already exists")
  27. fake_db[item.id] = item
  28. return item

扩展测试文件

有一个test_main_b.py文件

  1. from fastapi.testclient import TestClient
  2. from .main_b import app
  3. client = TestClient(app)
  4. def test_read_item():
  5. response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
  6. assert response.status_code == 200
  7. assert response.json() == {
  8. "id": "foo",
  9. "title": "Foo",
  10. "description": "There goes my hero",
  11. }
  12. def test_read_item_bad_token():
  13. response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
  14. assert response.status_code == 400
  15. assert response.json() == {"detail": "Invalid X-Token header"}
  16. def test_read_inexistent_item():
  17. response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
  18. assert response.status_code == 404
  19. assert response.json() == {"detail": "Item not found"}
  20. def test_create_item():
  21. response = client.post(
  22. "/items/",
  23. headers={"X-Token": "coneofsilence"},
  24. json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
  25. )
  26. assert response.status_code == 200
  27. assert response.json() == {
  28. "id": "foobar",
  29. "title": "Foo Bar",
  30. "description": "The Foo Barters",
  31. }
  32. def test_create_item_bad_token():
  33. response = client.post(
  34. "/items/",
  35. headers={"X-Token": "hailhydra"},
  36. json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
  37. )
  38. assert response.status_code == 400
  39. assert response.json() == {"detail": "Invalid X-Token header"}
  40. def test_create_existing_item():
  41. response = client.post(
  42. "/items/",
  43. headers={"X-Token": "coneofsilence"},
  44. json={
  45. "id": "foo",
  46. "title": "The Foo ID Stealers",
  47. "description": "There goes my stealer",
  48. },
  49. )
  50. assert response.status_code == 400
  51. assert response.json() == {"detail": "Item already exists"}

每当你需要客户端在请求中传递信息,而你又不知道如何传递时,你可以搜索(Google)如何在requests中传递信息。

那你就在你的测试中做同样的事情:

  • 要传递一个路径或查询参数,将其添加到URL本身。
  • 要传递一个JSON体,需要向参数json传递一个Python对象(例如dict)。
  • 如果你需要发送 Form Data而不是JSON,请使用data参数代替。
  • 要传递头文件,请在头文件参数中使用dict。
  • 对于cookie,在cookie参数中的dict。

关于如何将数据传递到后端(使用请求或TestClient)的更多信息,请查看 Requests documentation.

:::info info
请注意,TestClient接收的是可以转换为JSON的数据,而不是Pydantic模型。
如果您的测试中有一个Pydantic模型,并且您想在测试期间将其数据发送到应用程序,您可以使用JSON Compatible Encoder中描述的 JSON Compatible Encoder. :::

运行

需要安装pytest

  1. pip install pytest

它将自动检测文件和测试,执行它们,并将结果报告给你。
运行测试与:

  1. pytest
  2. ================ test session starts ================
  3. platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
  4. rootdir: /home/user/code/superawesome-cli/app
  5. plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
  6. collected 6 items
  7. ████████████████████████████████████████ 100%
  8. test_main.py ...... [100%]
  9. ================= 1 passed in 0.03s =================