FastAPI 新手入门第 12 篇:不靠手点 /docs,用测试确认接口没坏

FastAPI 新手入门第 12 篇:不靠手点 /docs,用测试确认接口没坏
到目前为止我们验证接口的方式都是启动服务打开/docs手动填入参数点调用。这个方法有两个问题一是慢每次改了代码都要重新打开浏览器试一遍二是不可重复你试了三个接口后端还会多出几个你很难知道自己有没有漏测。这篇我把测试加上。用 FastAPI 自带的TestClient不启动真实服务也能发 HTTP 请求。写好几条测试命令行跑一遍就能确认所有接口都正常。做完后你在终端敲一条命令十几条断言自动跑完全部通过。为什么测试不是高级话题很多人觉得测试是代码写完之后再做的高级优化。这个想法在前后端开发里都挺常见的但放在后端接口开发上尤其危险。接口是对外的。前端调用你写的接口改了转发逻辑、加了字段校验、换了数据库操作方式接口行为都可能变。如果每次依赖肉眼在/docs里点一遍大概率会漏。测试代码解决的问题很具体你改了代码跑一条测试确认接口行为跟你预期的一样。这就够了。先装依赖需要两个包pytest是测试框架httpx是TestClient背后真正发 HTTP 请求的库pip install pytest httpx然后在pyproject.toml里加上dependencies [ fastapi[standard]0.115.0, pydantic-settings2.0.0, sqlalchemy2.0, pytest8.0, httpx0.27, ]装上依赖之后用pytest命令就能自动发现项目里的测试文件。创建 tests 目录和测试夹具新建tests/__init__.py空文件就行然后在tests/conftest.py里写测试夹具。conftest.py是 pytest 的特殊文件。写在里面的 fixture 会被同目录下的所有测试文件自动共享。我先写两个 fixtureimportpytestfromfastapi.testclientimportTestClientfromsqlalchemyimportcreate_enginefromsqlalchemy.ormimportsessionmakerfromapp.databaseimportBase,get_dbfromapp.mainimportapp TEST_DATABASE_URLsqlite:///./test.dbenginecreate_engine(TEST_DATABASE_URL,connect_args{check_same_thread:False},)TestingSessionLocalsessionmaker(autocommitFalse,autoflushFalse,bindengine)pytest.fixturedefdb():Base.metadata.create_all(bindengine)dbTestingSessionLocal()try:yielddbfinally:db.close()Base.metadata.drop_all(bindengine)pytest.fixturedefclient(db):defoverride_get_db():yielddb app.dependency_overrides[get_db]override_get_dbwithTestClient(app)asc:yieldc app.dependency_overrides.clear()重点看两个地方。dbfixture 做了三件事建表、给测试用例一个数据库会话、用例跑完后删表。这样每个测试用例都用干净的数据库开始不会互相影响。clientfixture 里最关键的一行app.dependency_overrides[get_db]override_get_db这行告诉 FastAPI“当接口函数里写了Depends(get_db)时换成用测试数据库别用线上数据库”。这样测试跑的时候创建的商品写到test.db里不会影响fastapi_beginner_lab.db。app.dependency_overrides是 FastAPI 为测试专门设计的。它让你能在不修改接口代码的前提下替换任何依赖。写第一条测试确认服务能跑新建tests/test_health.pydeftest_read_root(client):responseclient.get(/)assertresponse.status_code200assertresponse.json(){message:Hello FastAPI}deftest_ping(client):responseclient.get(/ping)assertresponse.status_code200assertresponse.json(){message:pong}deftest_health(client):responseclient.get(/health)assertresponse.status_code200dataresponse.json()assertdata[status]okassertapp_nameindataassertenvindata这三条的写法一模一样client.get(/path)发一个 GET 请求。检查status_code是不是期望值。检查返回的 JSON 内容。client.get和client.post用起来就像requests库但不需要启动真实服务。写接口测试创建商品和 404新建tests/test_items.py。测试有状态的接口创建、查询比测试无状态接口首页、健康检查多一步需要处理 token 认证。deftest_create_item(client):responseclient.post(/items,json{name:Test Hammer,price:9.99,is_offer:False,},headers{x-token:secret-token},)assertresponse.status_code200dataresponse.json()assertdata[name]Test Hammerassertdata[price]9.99assertdata[is_offer]isFalseassertidindataassertcost_pricenotindataassertcreated_bynotindatadeftest_list_items(client):先创建一条数据再确认列表能查到它。client.post(/items,json{name:Test Item,price:5.0},headers{x-token:secret-token},)responseclient.get(/items,headers{x-token:secret-token})assertresponse.status_code200dataresponse.json()assertisinstance(data,list)assertlen(data)1deftest_read_item_not_found(client):responseclient.get(/items/99999,headers{x-token:secret-token})assertresponse.status_code404assertresponse.json()[detail]Item not founddeftest_missing_token_returns_401(client):responseclient.get(/items)assertresponse.status_code401几条测试覆盖了最常见的场景测试验证什么test_create_item创建成功响应字段正确内部字段没泄露test_list_items创建后能从列表查到test_read_item_not_found不存在的 ID 返回 404test_missing_token_returns_401不带 token 被拒绝headers{x-token: secret-token}因为我们的依赖get_token_header需要这个头部测试里要带上。运行测试在项目根目录下pytest输出类似这样tests/test_health.py ... [ 37%] tests/test_items.py .... [100%]如果某个测试失败pytest 会精确告诉你是哪一行断言不通过以及实际值和期望值的差异。想看得详细一点加-vpytest-v测试和 /docs 不是二选一有了测试不是说你就不用/docs了。/docs更适合在开发时代替网页客户端快速试试接口能不能走通。测试更适合在每次改完代码后确认所有接口行为都没变。这两件事不冲突但不要把/docs当做测试的替代品。动手改一下给测试加一条验证创建商品时不带必填字段会返回 422deftest_create_item_missing_name(client):responseclient.post(/items,json{price:9.99},headers{x-token:secret-token},)assertresponse.status_code422跑pytest确认这条测试通过。如果能自动验证 422 错误就说明你已经能给接口写测试了。到这里这篇的目标已经完成我们用 TestClient 写了健康检查和商品接口的测试。我们用 dependency_overrides 让测试用独立的数据库。我们运行 pytest所有测试通过。本文代码https://github.com/tanghaojin/fastapi-beginner-lab/tree/article-12-testing下一篇解决另一个问题现在接口认证靠写死secret-token下一篇换成真正的登录流程。参考资料FastAPI Testing: https://fastapi.tiangolo.com/tutorial/testing/pytest 文档: https://docs.pytest.org/