Appearance
接口测试
手动用浏览器或 curl 测接口,改一次代码测一遍——接口多了根本测不过来。自动化测试是写一遍测试代码,之后每次改代码跑一下,几秒钟确认所有接口还没坏。FastAPI 自带测试工具,不用启动真实服务器。
一、pytest 基础
Python 最主流的测试框架是 pytest。写一个函数、用 assert 断言、pytest 自动发现和执行:
bash
uv add pytestpython
# test_math.py
def test_add():
assert 1 + 1 == 2
def test_string_upper():
assert "hello".upper() == "HELLO"运行:
bash
uv run pytestpytest 自动找以 test_ 开头的文件和函数,运行里面的 assert。断言失败显示具体哪一行、预期值和实际值。
二、fixture——测试前准备、测试后清理
测试接口需要数据库连接、测试数据、清理数据。每次手写太重复。fixture 是 pytest 的依赖注入机制——写一次,多个测试复用:
python
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from models import Base, Article
@pytest.fixture
def db_session():
"""每个测试用独立的内存数据库,测试完自动销毁。"""
# 用 SQLite 内存数据库,不碰真实数据库文件
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session # 把 session 交给测试函数用
# 测试结束后清理
session.close()
def test_create_article(db_session):
"""测试创建文章。db_session 参数自动注入 fixture 返回的 session。"""
article = Article(title="测试文章", content="内容", status="published")
db_session.add(article)
db_session.commit()
# 断言:数据库里确实有一条
articles = db_session.query(Article).all()
assert len(articles) == 1
assert articles[0].title == "测试文章"@pytest.fixture 标注的函数是 fixture。测试函数的参数名跟 fixture 名字一样(db_session),pytest 自动注入。yield 前面的代码是准备,yield 后面的代码是清理——每个测试拿到全新的内存数据库,互不干扰。
sqlite:///:memory: 是 SQLite 内存模式——数据库存在内存里,不写文件,连接关闭就消失。用它做测试数据库,速度快且不污染开发库。
三、TestClient——测 FastAPI 接口
FastAPI 基于 Starlette,自带 TestClient——不用启动 uvicorn,直接在进程内调用 app:
python
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_list_articles():
"""测试 GET /articles。"""
response = client.get("/articles")
assert response.status_code == 200
data = response.json()
assert "items" in data
def test_create_article():
"""测试 POST /articles。"""
response = client.post(
"/articles",
json={"title": "新文章", "content": "正文内容"},
)
assert response.status_code == 200
assert response.json()["title"] == "新文章"
def test_get_article_not_found():
"""测试查不存在的文章返回 404。"""
response = client.get("/articles/999")
assert response.status_code == 404TestClient(app) 包装了 FastAPI app,client.get("/articles") 就像发了一个真实的 HTTP 请求,但不走网络——直接在内存里调用路由函数。响应的 status_code 和 json() 跟真实请求完全一样。
测试正常流程(创建成功)、异常流程(查不到返回 404)、边界条件(参数格式错误返回 422)——这三种测全了,接口的基本质量就有保证。
四、测试需要登录的接口
需要 Token 的接口,测试时要带认证。两种做法:
1 先登录拿 Token
python
def test_get_profile():
"""测试需要登录的接口。"""
# 先注册并登录
client.post("/register", json={"username": "testuser", "password": "testpass"})
login_resp = client.post(
"/login",
data={"username": "testuser", "password": "testpass"},
)
token = login_resp.json()["access_token"]
# 带 Token 访问需要认证的接口
response = client.get(
"/me",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
assert response.json()["username"] == "testuser"2 用 fixture 直接签发 Token
如果登录逻辑复杂,测试时直接签发 Token 跳过登录:
python
@pytest.fixture
def auth_headers():
"""直接签发一个测试用 Token。"""
from auth import create_token
token = create_token({"sub": "1", "username": "testuser"})
return {"Authorization": f"Bearer {token}"}
def test_create_article_authed(auth_headers):
"""测试登录后创建文章。"""
response = client.post(
"/articles",
json={"title": "测试", "content": "内容"},
headers=auth_headers,
)
assert response.status_code == 200fixture 直接签发 Token 更快,不用走完整登录流程。但要确保 create_token 函数的签名不变,否则 fixture 和真实逻辑会脱节。
五、测试要测什么
| 类型 | 测什么 | 示例 |
|---|---|---|
| 正常流程 | 预期操作返回预期结果 | 创建文章 → 200,数据库有记录 |
| 异常流程 | 错误操作返回正确错误码 | 查不存在的 ID → 404 |
| 参数校验 | 参数格式错误被拦截 | 标题为空 → 422 |
| 权限控制 | 未登录或权限不足被拒绝 | 不带 Token → 401 |
| 边界条件 | 边界值处理正确 | 分页 limit=0 或 limit=999999 |
核心原则:每个接口至少测正常流程和异常流程各一条。不追求 100% 覆盖率,但关键接口(认证、增删改)的测试不能少。
六、运行测试
bash
# 运行所有测试
uv run pytest
# 只看结果,不显示详细输出
uv run pytest -q
# 测试某个文件
uv run pytest test_articles.py
# 测试某个函数
uv run pytest test_articles.py::test_create_article
# 显示 print 输出(调试时用)
uv run pytest -sCI/CD 流水线里加一步 uv run pytest——代码推上去自动跑测试,测试不过不让部署。这是持续集成的基本保障。