(二十三)用接口测试检查输入、权限与持久化
1. 每个测试使用自己的数据库
第六章已经用 pytest 检查文件和事务,这里把检查入口换成 HTTP。临时目录建立 SQLite 文件,with TestClient(...) 保证应用启动与收尾,账号摘要在整组测试中计算一次。
# 项目文件:api_project/tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from werkzeug.security import generate_password_hash
from bookapi.app import create_app
from bookapi.config import Settings
from bookapi.models import User
@pytest.fixture(scope="session")
def password_hashes():
# 整组测试复用摘要,仍由真正的密码校验函数检查登录。
return {name: generate_password_hash(f"learn-{name}") for name in ["alice", "bob"]}
@pytest.fixture
def app(tmp_path, password_hashes):
application = create_app(Settings(tmp_path / "books.sqlite3"))
with TestClient(application) as client:
with application.state.session_factory.begin() as session:
session.add_all(
[
User(username=name, password_hash=value)
for name, value in password_hashes.items()
]
)
yield application, client
def login(client, username="alice"):
response = client.post(
"/auth/login", json={"username": username, "password": f"learn-{username}"}
)
assert response.status_code == 200
return {"Authorization": f"Bearer {response.json()['access_token']}"}
@pytest.fixture
def alice(app):
return login(app[1])
@pytest.fixture
def bob(app):
return login(app[1], "bob")login 通过真实登录路由取得令牌,测试继续通过请求头发送。读取、修改和权限判断都经过实际依赖链,没有把当前用户替换成固定值。
测试内容见 书籍与输入测试、账号测试 和 资源、异常与重开测试。它们分别核对:
| 过程 | 观察结果 |
|---|---|
| 新增 → 读取 → 分页 → 部分更新 → 删除 | 状态、字段、Location 与最后的记录 |
| 错误类型、空白书名、附加归属、null | 422,记录数量和内容保持不变 |
| 两个账号管理同名书与交叉修改 | 各自可保存,同一编号的越权操作被拒绝 |
| 同名新增与同名改名 | 409,之后仍可读取原有记录 |
| 到期、退出和缺少令牌 | 401 和认证提示头 |
| 数据库 INSERT 执行后发生异常 | 500,新会话核对记录没有留下 |
| 一条请求的数据库依赖复用 | 只创建一份会话,正常和失败均收尾 |
| 关闭应用再打开同一个文件 | 账号、有效令牌和书籍均能恢复 |
| 后台导出与另一用户读取 | 完成结果可读,归属不同得到 403 |
# 在 api_project 中运行整组测试。
uv run python -m pytest -q
2. 覆盖依赖时保留核验对象
框架提供 app.dependency_overrides[原函数] = 替代函数,替换某项依赖。配套资源测试只在 get_session 外包一层记录动作的生成器;认证仍校验真实令牌,业务仍访问临时 SQLite。身份依赖和路由共同请求这项资源,记录中只出现一次 open 和一次 exit。
依赖覆盖在 finally 中清空,让后面的测试使用原来的配置。请求本身出错时,也核对连接已经归还。这里检查的是资源生命周期与失败状态,避免测试只根据返回 200 判断程序正常。