P17第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

接口与逻辑测试

接口与逻辑测试 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这一课给 P08-P16 攒下的接口装上安全网:用 pytest + TestClient 覆盖成功、输入错误、重复写入、外部服务失败四类场景,全程用临时数据库,绝不碰你真实的学习记录。

问题场景

P13 改过分页逻辑,你手动 curl 了六遍才敢重启服务;P15 改了错误分类,前端联调时才发现 cancelled 事件没发。这些验证每次都要人来点,人就会漏。测试的价值不是「显得专业」,是把六遍 curl 固化成毫秒级可重复的脚本。

核心解释

FastAPI 官方路径是 TestClient:不需要真的起 uvicorn,进程内直接调路由,连接口异常都能断言。

# conftest.py —— fixture:每个测试拿独立的临时库
import pytest
import sqlite3
from fastapi.testclient import TestClient
from main import app, get_db

@pytest.fixture
def client(tmp_path):
    conn = sqlite3.connect(tmp_path / "test.db")   # pytest 内置的临时目录
    conn.row_factory = sqlite3.Row
    init_schema(conn)
    app.dependency_overrides[get_db] = lambda: conn
    with TestClient(app) as c:
        yield c
    app.dependency_overrides.clear()               # 测试完拆掉,防止污染下一个测试

# test_notes.py —— 四类场景
def test_create_success(client):
    res = client.post("/notes", json={"title": "函数与模块", "body": "拆分心得"})
    assert res.status_code == 201
    assert res.json()["title"] == "函数与模块"       # 断言可见行为,不只断言状态码

def test_create_invalid_input(client):
    res = client.post("/notes", json={"title": "", "body": "x"})
    assert res.status_code == 422
    assert res.json()["error"]["category"] == "invalid_param"   # P13 的统一错误结构

def test_create_duplicate(client):
    client.post("/notes", json={"title": "A", "body": "x"})
    res = client.post("/notes", json={"title": "A", "body": "y"})
    assert res.status_code == 409

def test_upstream_failure(client, monkeypatch):
    monkeypatch.setattr("main.fetch_one", fake_timeout)         # 替换外部依赖
    res = client.post("/ai/chat", json={"message": "hi"})
    assert res.status_code == 200
    assert "unreachable" in res.text                # 上游失败也走 SSE error 事件

四条纪律对应四类场景之外的真实收益:

  1. fixture 管隔离。tmp_path 是 pytest 内置的临时目录(每测试唯一),配 P09 的 dependency_overrides 换掉数据库——测试从头到尾不碰 reactlab.db,你的真实学习记录绝对安全。这条是硬红线。
  2. 断言行为不是断言实现。断言「返回 201 且 body 里标题正确」,而不是断言内部调了哪个函数;重构内部实现时测试不该跟着改。
  3. 外部依赖一律替换。P10 的超时、P15 的上游,用 monkeypatch 换成假实现,测试才稳定可控——网络抖动不该决定测试红绿。
  4. 错误路径是测试的主体。422/409/上游失败的用例数量应该超过成功用例;成功路径只有一条,失败路径无穷多。

示例拆解

跑起来看输出:

pip install pytest httpx
pytest -v
# test_create_success PASSED
# test_create_invalid_input PASSED
# test_create_duplicate PASSED
# test_upstream_failure PASSED
# 4 passed in 0.3s

验证隔离是否真生效:故意在 conftest.py 里把 override 注释掉跑一次,测试会去连真库(或报错)——立刻改回来。这一步的意义是确认「测试不碰真实数据」不是嘴上说说,而是由 override 机制保证的。之后每次改接口先 pytest,绿了再联调,前端同学(就是你自己)会感谢这套红绿灯。

动手任务

  1. 给 /courses 分页补测试:limit=500 返回 422、page=999 返回空 items 且 200——覆盖 P13 的两个边界。
  2. 用 monkeypatch 让上传解析必然失败,断言文档状态变 failed 且 error 字段非空(P14 的失败留痕)。
  3. 故意删掉 dependency_overrides.clear(),连续跑两次 pytest 观察是否互相污染,然后加回来。

常见错误

  • 测试连真实数据库:没有 override 的 TestClient 会按生产配置连库,学习数据被测试数据污染;conftest 里强制临时库。
  • 只测成功路径:四个 200 覆盖不了任何真实风险;每写一个成功用例,配一个失败用例。
  • 测试间共享状态:模块级列表当「数据库」,跑单测过跑全套挂;每个测试独立 fixture。
  • 断言过深:断言内部函数被调用、断言 dict 完全相等,改个字段全红;断言外部可见行为。

检查与复盘

自查清单:

  • pytest 全绿且耗时秒级,可以当作每次改代码后的习惯动作
  • 确认测试运行期间真实 reactlab.db 没有被写入
  • 四类场景(成功/输入错/重复/上游失败)各有至少一个用例

参考资料

检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码简单

阅读代码(pytest,Python 3.11+,文件为 test_calc.py,pytest 直接运行)。几个通过、几个失败?

def add(a, b):
    return a + b

def test_add_normal():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, -1) == -2

def test_add_string():
    assert add('a', 'b') == 'ab'

def test_add_wrong():
    assert add(1, 2) == 4
Q2多选简单(多选,全部选对才算正确)

用 fastapi.testclient.TestClient 测试 FastAPI 接口(Python 3.11+),下列说法正确的有哪些?(多选)

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...