FDE Training · Module 17
17
测试与质量保障 · 让客户敢用、敢上生产
模块 17 · 测试与质量保障
本章目标:FDE 交付的东西不能是"我本地跑通了"就完事。客户要的是出了问题能兜住、改了东西不会搞坏老功能。这一章讲 FDE 现场最实用的测试策略:单元测试、接口测试、LLM 应用专属评测、CI 自动化。
质量保障闭环:写码 → 三层自动测试 → CI 卡质量门 → 安全发版
17.1 为什么 FDE 必须写测试
很多 FDE 觉得"我就一个人写脚本,测试是浪费时间"。错。原因:
- 客户项目改需求频繁,改 A 坏 B 是家常便饭。测试就是你的安全网。
- 你不是唯一维护的人——三个月后客户那边的工程师接手,没测试他不敢改。
- 上线前手动点一遍要几小时,CI 跑自动测试只要几分钟。
80% 的测试精力应该花在最核心、最容易出 bug 的路径上,不要追求 100% 覆盖率。一个内部 CRUD 后台写一堆测试是过度工程。
17.2 测试金字塔:三层怎么分配精力
| 层级 | 测什么 | 数量 | 速度 | 例子 |
|---|---|---|---|---|
| 单元测试 | 单个函数/类逻辑 | 多(最多) | 毫秒级 | 金额计算、字符串处理 |
| 接口测试 | API 输入输出 | 中 | 秒级 | POST /orders 返回 201 |
| E2E 测试 | 完整用户流程 | 少(最少) | 分钟级 | 用户从注册到下单全流程 |
金字塔原理:底层多、快、便宜;上层少、慢、贵。别反过来——写一堆慢的 E2E,单元测试一个没有。
17.3 单元测试:给核心逻辑写测试
用 pytest 写一个典型例子:
# calc.py
def calc_order_total(items, tax_rate=0.06):
subtotal = sum(it["price"] * it["qty"] for it in items)
tax = subtotal * tax_rate
return round(subtotal + tax, 2)
# test_calc.py
import pytest
from calc import calc_order_total
def test_basic():
items = [{"price": 100, "qty": 2}, {"price": 50, "qty": 1}]
assert calc_order_total(items) == 265.0 # 250 * 1.06 = 265
def test_empty():
assert calc_order_total([]) == 0
def test_zero_tax():
items = [{"price": 100, "qty": 1}]
assert calc_order_total(items, tax_rate=0) == 100
# 跑:pytest test_calc.py -v
写测试的技巧:
- 测行为不测实现:输入什么 → 输出什么,不要测内部变量。
- 一个测试一个断言场景:别一个函数里测八件事,挂了不知道哪个错。
- 边界值必测:空列表、0、负数、超长输入。
17.4 接口测试:给 API 写自动测试
import requests
BASE = "http://localhost:8000"
def test_create_order():
resp = requests.post(f"{BASE}/orders", json={
"user_id": 1,
"items": [{"sku": "A1", "qty": 2}],
})
assert resp.status_code == 201
data = resp.json()
assert data["order_id"] > 0
assert data["status"] == "pending"
def test_get_order_not_found():
resp = requests.get(f"{BASE}/orders/99999")
assert resp.status_code == 404
def test_unauthorized():
resp = requests.get(f"{BASE}/orders")
assert resp.status_code == 401
接口测试要测的关键点:
- 正常返回 200/201。
- 参数错返回 400。
- 不存在返回 404。
- 未登录返回 401。
- 权限不够返回 403。
17.5 LLM 应用专属测试:Evals
大模型应用的测试和传统软件不一样——输出不是固定的,你不能 assert 它一定输出某句话。这就是模块 06 讲的 Evals。FDE 现场实用做法:
17.5.1 建一个小评测集
# eval_set.py
EVAL_CASES = [
{
"question": "违约金是多少?",
"context": "合同第3条:违约金为本金的10%",
"expected_keywords": ["10%", "违约金"],
},
{
"question": "能不能退款?",
"context": "合同第7条:7天内无理由退款",
"expected_keywords": ["7天", "退款"],
},
]
def run_eval():
passed = 0
for case in EVAL_CASES:
answer = ask_rag(case["question"], case["context"])
# 检查答案里有没有命中关键词
hits = sum(1 for kw in case["expected_keywords"] if kw in answer)
if hits == len(case["expected_keywords"]):
passed += 1
else:
print(f"FAIL: {case['question']} -> {answer}")
print(f"通过率: {passed}/{len(EVAL_CASES)}")
17.5.2 改 Prompt 后必跑回归
每次改 Prompt、换模型、调 RAG 参数,都跑一遍这个评测集。通过率掉了就说明改坏了,别直接上线。
17.6 CI 自动化:提交代码自动跑测试
GitHub Actions 示例:push 代码后自动跑测试,不过就拦住:
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: {python-version: "3.11"}
- run: pip install -r requirements.txt pytest
- run: pytest tests/ -v --tb=short
CI 的意义:测试不过不能合并到主分支、不能部署。把"靠自觉跑测试"变成"不跑就不让过"。
17.7 动手练习(可折叠答案)
练习 1:给"计算订单总价"函数写单元测试,要覆盖哪些场景?
答案思路:
- 正常情况:多件商品算总价。
- 空购物车:总价 0。
- 税率为 0:不打税。
- 数量为 0:该商品不计价。
- 价格有小数:精度对不对。
练习 2:LLM RAG 应用怎么测"回答得好不好"?
答案思路:
- 建评测集:20-50 个典型问题 + 标准答案要点。
- 跑一遍,看答案是否命中要点。
- 加一个"资料没有的问题"测试,看模型会不会瞎编。
- 改 Prompt 后跑回归,通过率不下降才上线。
练习 3:为什么 E2E 测试要少写?
答案思路:
- E2E 慢——一个流程跑几分钟,CI 等不起。
- E2E 脆——前端改个按钮 id 就挂,维护成本高。
- E2E 定位难——挂了不知道是哪层的问题。
- 正确姿势:核心流程留 3-5 个 E2E,其余靠单元+接口测试覆盖。
17.8 本章面试题
- "测试金字塔是什么?为什么?"→ 答:单元测试多而快(底层),接口测试中等,E2E 少而慢(顶层)。这样测试套件跑得快、维护成本低,又能覆盖关键路径。
- "LLM 应用怎么测?不能断言固定输出。"→ 答:建评测集,用关键词命中、参考答案匹配、或用更强的模型当裁判打分。改 Prompt/模型后跑回归,通过率不下降才上线。
- "CI 里测试不过怎么办?"→ 答:不能合并、不能部署。先修测试或者修代码,别跳过测试硬发。跳过一次,后面就会跳过无数次。
- "追求 100% 覆盖率好不好?"→ 答:不好。覆盖率只是数字,不重要逻辑的测试写得再多也没价值。核心业务路径测好,边缘代码能手动测到就行。
17.9 小结
- 测试是 FDE 的安全网:改需求不怕、交接不怕、上线有底气。
- 测试金字塔:多写单元、适量接口、少写 E2E。
- LLM 应用用 Evals 测:建评测集、关键词/裁判打分、改完跑回归。
- CI 把测试变成强制门槛:不过就不让发版。
- 不要追求 100% 覆盖率,把核心路径测好就够。
本页由 FDE 培养课程文档生成,完整课程见 FDE 培养 · 课程总览。