三周精通FastAPI:32 探索如何使用pytest进行高效、全面的项目测试!

news/2024/11/14 3:02:27/

官方文档:https://fastapi.tiangolo.com/zh/tutorial/testing/

进行项目测试¶

感谢 Starlette,测试FastAPI 应用轻松又愉快。

它基于 HTTPX, 而HTTPX又是基于Requests设计的,所以很相似且易懂。

有了它,你可以直接与FastAPI一起使用 pytest。

使用 TestClient

"信息"

要使用 TestClient,先要安装 httpx.

例:pip install httpx.

导入 TestClient.

通过传入你的FastAPI应用创建一个 TestClient 。

创建名字以 test_ 开头的函数(这是标准的 pytest 约定)。

像使用 httpx 那样使用 TestClient 对象。

为你需要检查的地方用标准的Python表达式写个简单的 assert 语句(重申,标准的pytest)。

python">from fastapi import FastAPI
from fastapi.testclient import TestClientapp = FastAPI()@app.get("/")
async def read_main():return {"msg": "Hello World"}client = TestClient(app)def test_read_main():response = client.get("/")assert response.status_code == 200assert response.json() == {"msg": "Hello World"}

"提示"

注意测试函数是普通的 def,不是 async def

还有client的调用也是普通的调用,不是用 await

这让你可以直接使用 pytest 而不会遇到麻烦。

"技术细节"

你也可以用 from starlette.testclient import TestClient

FastAPI 提供了和 starlette.testclient 一样的 fastapi.testclient,只是为了方便开发者。但它直接来自Starlette。

"提示"

除了发送请求之外,如果你还想测试时在FastAPI应用中调用 async 函数(例如异步数据库函数), 可以在高级教程中看下 Async Tests 。

分离测试¶

在实际应用中,你可能会把你的测试放在另一个文件里。

您的FastAPI应用程序也可能由一些文件/模块组成等等。

fastapi-app">FastAPI app 文件¶

假设你有一个像 更大的应用 中所描述的文件结构:

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

在 main.py 文件中你有一个 FastAPI app:

python">from fastapi import FastAPIapp = FastAPI()@app.get("/")
async def read_main():return {"msg": "Hello World"}

测试文件¶

然后你会有一个包含测试的文件 test_main.py 。app可以像Python包那样存在(一样是目录,但有个 __init__.py 文件):

python">.
├── app
│   ├── __init__.py
│   ├── main.py
│   └── test_main.py

因为这文件在同一个包中,所以你可以通过相对导入从 main 模块(main.py)导入app对象:

python">from fastapi.testclient import TestClientfrom .main import appclient = TestClient(app)def test_read_main():response = client.get("/")assert response.status_code == 200assert response.json() == {"msg": "Hello World"}

...然后测试代码和之前一样的。

测试:扩展示例¶

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

fastapi-app_1">扩展后的 FastAPI app 文件¶

让我们继续之前的文件结构:

python">.
├── app
│   ├── __init__.py
│   ├── main.py
│   └── test_main.py

假设现在包含FastAPI app的文件 main.py 有些其他路径操作

有个 GET 操作会返回错误。

有个 POST 操作会返回一些错误。

所有路径操作 都需要一个X-Token 头。

python">from typing import Annotatedfrom fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModelfake_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()class Item(BaseModel):id: strtitle: strdescription: str | None = None@app.get("/items/{item_id}", response_model=Item)
async def read_main(item_id: str, x_token: Annotated[str, Header()]):if x_token != fake_secret_token:raise HTTPException(status_code=400, detail="Invalid X-Token header")if item_id not in fake_db:raise HTTPException(status_code=404, detail="Item not found")return fake_db[item_id]@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: Annotated[str, Header()]):if x_token != fake_secret_token:raise HTTPException(status_code=400, detail="Invalid X-Token header")if item.id in fake_db:raise HTTPException(status_code=409, detail="Item already exists")fake_db[item.id] = itemreturn item

扩展后的测试文件¶

然后您可以使用扩展后的测试更新test_main.py

python">from fastapi.testclient import TestClientfrom .main import appclient = TestClient(app)def test_read_item():response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})assert response.status_code == 200assert response.json() == {"id": "foo","title": "Foo","description": "There goes my hero",}def test_read_item_bad_token():response = client.get("/items/foo", headers={"X-Token": "hailhydra"})assert response.status_code == 400assert response.json() == {"detail": "Invalid X-Token header"}def test_read_nonexistent_item():response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})assert response.status_code == 404assert response.json() == {"detail": "Item not found"}def test_create_item():response = client.post("/items/",headers={"X-Token": "coneofsilence"},json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},)assert response.status_code == 200assert response.json() == {"id": "foobar","title": "Foo Bar","description": "The Foo Barters",}def test_create_item_bad_token():response = client.post("/items/",headers={"X-Token": "hailhydra"},json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},)assert response.status_code == 400assert response.json() == {"detail": "Invalid X-Token header"}def test_create_existing_item():response = client.post("/items/",headers={"X-Token": "coneofsilence"},json={"id": "foo","title": "The Foo ID Stealers","description": "There goes my stealer",},)assert response.status_code == 409assert response.json() == {"detail": "Item already exists"}

每当你需要客户端在请求中传递信息,但你不知道如何传递时,你可以通过搜索(谷歌、百度)如何用 httpx做,或者是用 requests 做,毕竟HTTPX的设计是基于Requests的设计的。

还可以在FastAPI启动服务后,从docs文档中,点开GET、POST等路径说明,点击右上角的“Try it out”, docs文档会给出curl请求的参数示例。

接着只需在测试中同样操作。

示例:

  • 传一个路径 或查询 参数,添加到URL上。
  • 传一个JSON体,传一个Python对象(例如一个dict)到参数 json
  • 如果你需要发送 Form Data 而不是 JSON,使用 data 参数。
  • 要发送 headers,传 dict 给 headers 参数。
  • 对于 cookies,传 dict 给 cookies 参数。

关于如何传数据给后端的更多信息 (使用httpx 或 TestClient),请查阅 HTTPX 文档.

"信息"

注意 TestClient 接收可以被转化为JSON的数据,而不是Pydantic模型。

如果你在测试中有一个Pydantic模型,并且你想在测试时发送它的数据给应用,你可以使用在JSON Compatible Encoder介绍的jsonable_encoder 。

运行起来¶

之后,你只需要安装 pytest:

fast →pip install pytest
████████████████████████████████ 80%

他会自动检测文件和测试,执行测试,然后向你报告结果。

执行测试:

fast →pytest
================ test session starts ================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/user/code/superawesome-cli/app
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
collected 6 items

██████████████████████████████████████ 95%

实践

 首先安装httpx

pip install httpx

写文件代码

将下面代码存为main.py

python">from typing import Annotatedfrom fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModelfake_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()class Item(BaseModel):id: strtitle: strdescription: str | None = None@app.get("/items/{item_id}", response_model=Item)
async def read_main(item_id: str, x_token: Annotated[str, Header()]):if x_token != fake_secret_token:raise HTTPException(status_code=400, detail="Invalid X-Token header")if item_id not in fake_db:raise HTTPException(status_code=404, detail="Item not found")return fake_db[item_id]@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: Annotated[str, Header()]):if x_token != fake_secret_token:raise HTTPException(status_code=400, detail="Invalid X-Token header")if item.id in fake_db:raise HTTPException(status_code=409, detail="Item already exists")fake_db[item.id] = itemreturn item

将下面代码存为test_main.py

python">from fastapi.testclient import TestClientfrom .main import appclient = TestClient(app)def test_read_item():response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})assert response.status_code == 200assert response.json() == {"id": "foo","title": "Foo","description": "There goes my hero",}def test_read_item_bad_token():response = client.get("/items/foo", headers={"X-Token": "hailhydra"})assert response.status_code == 400assert response.json() == {"detail": "Invalid X-Token header"}def test_read_nonexistent_item():response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})assert response.status_code == 404assert response.json() == {"detail": "Item not found"}def test_create_item():response = client.post("/items/",headers={"X-Token": "coneofsilence"},json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},)assert response.status_code == 200assert response.json() == {"id": "foobar","title": "Foo Bar","description": "The Foo Barters",}def test_create_item_bad_token():response = client.post("/items/",headers={"X-Token": "hailhydra"},json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},)assert response.status_code == 400assert response.json() == {"detail": "Invalid X-Token header"}def test_create_existing_item():response = client.post("/items/",headers={"X-Token": "coneofsilence"},json={"id": "foo","title": "The Foo ID Stealers","description": "There goes my stealer",},)assert response.status_code == 409assert response.json() == {"detail": "Item already exists"}

创建空白__init__.py文件

在当前目录创建文件:

python">touch __init__.py

启动服务

python">uvicorn main:app --reload

 测试:

在当前目录执行pytest

python">pytest

测试输出:

/Users/skywalk/py311/lib/python3.11/site-packages/pytest_asyncio/plugin.py:208: PytestDeprecationWarning: The configuration option "asyncio_default_fixture_loop_scope" is unset.

The event loop scope for asynchronous fixtures will default to the fixture caching scope. Future versions of pytest-asyncio will default the loop scope for asynchronous fixtures to function scope. Set the default fixture loop scope explicitly in order to avoid unexpected behavior in the future. Valid fixture loop scopes are: "function", "class", "module", "package", "session"

  warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET))

========================= test session starts =========================

platform darwin -- Python 3.11.4, pytest-8.3.3, pluggy-1.5.0

rootdir: /Users/skywalk/work/fastapi/test

plugins: asyncio-0.24.0, anyio-3.5.0

asyncio: mode=Mode.STRICT, default_loop_scope=None

collected 6 items                                                     

test_main.py ......                                             [100%]

========================== 6 passed in 0.99s ==========================

可以见到6项测试都通过了。

总结

pytest 提供了简洁的语法、灵活的夹具、丰富的插件和详细的调试信息,使得编写和维护测试变得更加容易和高效。无论是小型项目还是大型项目,pytest 都是一个非常强大且灵活的测试框架。

pytest 可以自动发现测试文件和测试用例,用户只需按照约定的命名规则(如以test_开头)命名文件和函数,pytest 会自动识别和运行这些测试。

具体pytest可以参考:测试框架pytest学习与实践_怎么使用 flake8-pytest-style-CSDN博客

调试

pytest报错

=============================== ERRORS ================================

____________________ ERROR collecting test_main.py ____________________

ImportError while importing test module '/Users/skywalk/work/fastapi/test/test_main.py'.

Hint: make sure your test modules/packages have valid Python names.

Traceback:

../../../py311/lib/python3.11/importlib/__init__.py:126: in import_module

    return _bootstrap._gcd_import(name[level:], package, level)

test_main.py:3: in <module>

    from .main import app

E   ImportError: attempted relative import with no known parent package

======================= short test summary info =======================

ERROR test_main.py

!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!

========================== 1 error in 1.14s ===========================

原来是目录下需要一个__init__.py文件,touch一个:

python">touch __init__.py

再执行pytest, 测试通过:

========================= test session starts =========================

platform darwin -- Python 3.11.4, pytest-8.3.3, pluggy-1.5.0

rootdir: /Users/skywalk/work/fastapi/test

plugins: asyncio-0.24.0, anyio-3.5.0

asyncio: mode=Mode.STRICT, default_loop_scope=None

collected 6 items                                                     

test_main.py ......                                             [100%]

========================== 6 passed in 0.99s ==========================


http://www.ppmy.cn/news/1546047.html

相关文章

GNU/Linux - /proc/sys/vm/overcommit_memory

/proc/sys/vm/overcommit_memory "是一个 Linux 内核参数&#xff0c;用于控制系统处理内存分配请求的方式。该参数对决定进程请求内存时内核的行为至关重要。让我们来详细了解一下它的含义和影响&#xff1a; The "/proc/sys/vm/overcommit_memory" is a Linux…

vue大疆建图航拍功能实现

介绍 无人机在规划一块区域的时候&#xff0c;我们需要手动的给予一些参数来影响无人机飞行&#xff0c;对于一块地表&#xff0c;无人机每隔N秒在空中间隔的拍照地表的一块区域&#xff0c;在整个任务执行结束后&#xff0c;拍到的所有区域照片能够完整的表达出一块地表&…

微服务设计模式 - 事件溯源模式(Event Sourcing Pattern)

微服务设计模式 - 事件溯源模式&#xff08;Event Sourcing Pattern&#xff09; 定义 事件溯源&#xff08;Event Sourcing&#xff09;是一种将所有状态更改保存为一系列事件的设计模式。每次系统状态发生变化时&#xff0c;都会生成一个事件&#xff0c;这些事件在事件存储…

【Rust设计模式之Fold模式】

Rust设计模式之Fold Fold &#xff08;折叠&#xff09; 如Rust Collection中的fold方法&#xff0c;是消耗迭代器适配器&#xff0c;将闭包应用于每一个元素&#xff0c;并将结果返回一样。Fold模式的中心思想也是如此&#xff0c;将元素折叠处理&#xff0c;最终计算出新的元…

关于QUERY_ALL_PACKAGES权限导致Google下架apk

谷歌商店被下架,原因是第三方使用了 QUERY_ALL_PACKAGES 权限&#xff1b; Google在高版本上限制了此权限的使用。当然&#xff0c;并不是 QUERY_ALL_PACKAGES 这个权限没有了&#xff0c;而是被列为敏感权限&#xff0c;必须有充分的理由说明&#xff0c;才允许上架 GP&#…

Bert快速入门

Python 语言 BERT 入门&#xff1a;让我们一起“吃透”BERT 1. 什么是 BERT&#xff1f; BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;是 Google 提出的预训练语言模型&#xff0c;它通过双向编码器理解文本中的上下文信息&#xf…

Hive-testbench套件使用文档

Hive-testbench套件使用文档 hive-testbench 是hortonworks的一个开源项目,用于测试和基准测试 Apache Hive 的工具集。它提供了一系列的测试数据集和查询样例,用于评估和比较 Hive 在不同配置和环境下的性能。hive-testbench 的主要目标是模拟真实的大规模数据集和复杂查询…

SpringBoot技术下的共享汽车运营平台

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理共享汽车管理系统的相关信息成为必然。开发…