"能跑"不等于"对"。测试是把"我觉得没问题"变成"有证据没问题"的 discipline。本页覆盖单元/集成/端到端测试、TDD、Mock、契约测试、性能压测、自动化与 CI、质量门禁,并以 pytest / JUnit 5 / Playwright / k6 为例。学完你能在任何项目里搭起一条"改一行代码就有测试兜底"的安全网,并且知道"测多少、测到哪一层"才是划算的。
测试类型与金字塔:钱该花在哪一层
测试不是越多越好,而是分层配比。经典"测试金字塔":底层大量单元测试(快、便宜),中层适量集成测试,顶层少量端到端(E2E)测试(慢、贵)。倒金字塔(E2E 一堆)会又慢又脆。这一章先把"有哪些测试、各自测什么、怎么配"讲清。
测试金字塔:钱该花在哪一层
金字塔的核心直觉是:越靠近代码底层,测试越便宜、越快、越稳;越往上越贵、越慢、越容易因环境问题随机失败。所以把保障重心放在底层,上层只覆盖最核心的用户路径。
| 层 | 测什么 | 速度/成本 | 占比 | 工具例 |
|---|---|---|---|---|
| 单元 Unit | 单个函数/类 | 快/低 | 70%+ | pytest、JUnit 5 |
| 集成 Integration | 模块间协作、数据库 | 中/中 | 20% | Testcontainers |
| 端到端 E2E | 整条用户路径 | 慢/高 | 5–10% | Playwright、Cypress |
论为什么反对"倒金字塔"
如果反着来——几百个 E2E、几乎没单测——每次改一行都要等几十分钟、还经常因为"测试环境抖了一下"而红。团队会逐渐不信任测试、手动跳过,安全网就废了。金字塔宁可多写单测,也别堆 E2E。
测试的全景:不止单元/集成/E2E
金字塔讲的是"功能测试"的分层。完整质量网还包括静态分析、契约、性能、混沌。别把它们混为一谈——每种回答不同问题。
| 测试类型 | 回答的问题 | 何时跑 |
|---|---|---|
| 静态分析 / Lint | 代码风格、明显坏味道 | 每次提交(最快) |
| 单元测试 | 这个函数逻辑对不对 | 每次提交 |
| 集成测试 | 拼起来协作对不对 | 每次提交 / 合并前 |
| 契约测试 | 服务间接口约定变没变 | 每次提交(消费者+提供方) |
| E2E 测试 | 用户真实路径通不通 | 合并后 / 预发 |
| 性能 / 压测 | 扛不扛得住流量 | 预发 / 定期 |
TDD:先写测试再写码
TDD(测试驱动开发)把"测试"从"写完才补"变成"写码之前先写"。流程是红 → 绿 → 重构:先写一个会失败的测试(红),写刚好让它通过的最少实现(绿),再在测试保护下重构。
论TDD 的真正价值不是"测试更多"
而是逼你把接口设计得可测——可测的代码天然低耦合、依赖清晰。先红后绿再重构,是小步快跑时的安全绳:每走一步都有红绿反馈,不会写着写着发现"这坨东西根本测不了"。
验收测试与回归测试:从"对不对"到"别搞坏"
两种常见视角:验收测试(Acceptance)站在业务角度验证"需求满足了吗"(常由 BDD 工具如 Cucumber 表达成"Given/When/Then");回归测试(Regression)则是把历史上所有发现过的 bug 固化成测试,保证"以前修好的别再犯"。金字塔里的每一层,最终都会沉淀为回归集的一部分。
论回归集是"会越长越慢"的
每个 bug 修完都该补一条测试。久而久之单测可能上千条——这正是金字塔的意义:单测快,几千条也能在几分钟内跑完;若都写成 E2E,CI 会慢到没人愿意跑。
怎么给团队定测试配比:风险驱动
金字塔是"默认推荐",但不是铁律。钱该花在"最容易错、错了最贵"的地方:支付/权限/核心算法要多写单测;UI 交互细节用少量 E2E 兜底即可。配比是可以按模块风险调整的,别机械照搬 70/20/10。
为了"看起来比例对"硬凑单测数量、或硬塞 E2E 凑占比,结果测试既慢又没价值。配比服务于"信心成本比",而不是数字本身。
静态分析与 Lint:最便宜的第一道关
功能测试之前,还有一道几乎零成本的质量关:静态分析。它在不运行代码的情况下揪出风格问题、未使用变量、明显坏味道,CI 里几秒跑完,是金字塔"尖"下面的地基。
论静态分析是"免费的安全网"
它不验证业务正确,但能挡住大量低级错误和风格争议("该用 tabs 还是 spaces"交给工具,别在 PR 里吵)。把它放在金字塔最底层、跑得最快,性价比最高。
BDD 与验收标准:测试从需求来
单测验证"函数对",但"函数对"不等于"需求满足"。BDD(行为驱动开发)用 Given/When/Then 把验收标准写成可执行用例,让产品、开发、测试说同一套语言。
论验收标准先对齐,再写码
很多返工源于"需求理解不一致"。把验收写成可执行的 BDD 场景,开发前就和产品的预期对齐——场景过了,需求就满足了,扯皮少一大半。
单元测试:Mock、覆盖率、边界与参数化
好单测遵循 AAA 结构:Arrange(准备)→ Act(执行)→ Assert(断言)。这一章讲清"单测到底测什么、怎么隔离外部依赖、覆盖率怎么读、边界和异常路径怎么覆盖",并给出 pytest 与 JUnit 5 两套示例。
AAA 结构:好单测长什么样
把测试体清晰分成三段:准备数据与环境、执行被测函数、断言结果。读起来像"给定…当…那么…",一眼能懂在测什么。
论一个测试只断言一件事
别在一个测试里塞十几个 assert 覆盖所有分支。一个测试聚焦一个行为,失败时才知道"到底哪坏了"。需要覆盖多分支,就写多个测试,名字说清场景。
测试替身家族:Dummy / Stub / Mock / Spy / Fake
"测试替身(Test Double)"是统称。它们长得像真实依赖,实际是替身。分清五种,才知道该用哪个。
| 替身 | 干什么 | 典型用途 |
|---|---|---|
| Dummy | 纯占位,从不调用 | 凑构造函数参数 |
| Stub | 返回预制答案 | 制造"依赖现在返回 X" |
| Mock | 预设期望 + 验证调用 | 验证"是否调了一次 charge" |
| Spy | 真做事 + 记录行为 | 记录发了几封邮件 |
| Fake | 轻量但真实实现 | 内存版数据库(更快) |
参数化测试:用一份逻辑测多种输入
同样的逻辑、不同输入输出,别复制粘贴十遍测试。参数化让一份测试体跑多组数据,哪组挂了一目了然。
边界值与异常路径:单测最易漏的地方
新手爱测 happy path(输入正常、返回正常),但真正的 bug 藏在边界和异常里:空值、0、负数、超长字符串、依赖抛错时我怎么处理。这些才是单测该重点覆盖的。
覆盖率 90% 但全是正常输入,等于"只验证了不会错的路径"。异常分支、边界值才是线上出事的高频区,务必显式覆盖。
覆盖率怎么读:行覆盖 / 分支覆盖 / 突变覆盖
覆盖率不是一个数,分几层。理解差别才不会误读报告。
| 覆盖率类型 | 含义 | 局限 |
|---|---|---|
| 行覆盖 Line | 多少行被执行过 | 执行了 ≠ 断言对了 |
| 分支覆盖 Branch | if/else 各分支是否都走 | 仍不保证逻辑正确 |
| 突变覆盖 Mutation | 故意改代码,测试能否变红 | 最严,但慢 |
JUnit 5 示例:同一套思想换 Java
金字塔和 AAA 是语言无关的。下面是 JUnit 5 版本,注意 @ParameterizedTest 和断言风格与 pytest 一一对应。
Fixture 与测试隔离:准备一次,多处复用
多个测试都要"一个干净的临时库 / 一个已登录用户",重复写很累。Fixture 把"准备+清理"抽出来,测试只关心自己那点逻辑,且每个测试独立、互不污染。
论隔离 = 可重复
fixture 的清理让每个测试"从同一起点出发",顺序无关、重复无关。这正是前面说的"可重复 = 可信任"——CI 随机跑、单独跑都该稳。
集成、E2E 与契约测试
单测保证"零件对",但零件拼起来可能对不上。集成测试验证"模块拼起来对不对"(如 API + 真实数据库);E2E 模拟用户点真实跑一遍;契约测试(Contract Test)专门守护"服务间接口约定"——消费者和提供方各自校验同一份契约,避免联调才爆雷。
集成测试:真连数据库,但用容器
集成测试不能 Mock 掉数据库(那还测什么集成),但也不能依赖"某台共享测试库"。最佳实践是每次测试起一个真实数据库容器(Testcontainers),用完即毁,环境 100% 干净可重现。
论为什么不用共享测试库
共享库会被别人/别的 CI 跑的数据污染,出现"我本地绿的、CI 红的"玄学。容器化让每次运行都从空库开始,确定性 = 可信任。
待测依赖怎么管:数据库 / HTTP 客户端
集成层要连的"外部"不止数据库,还有下游 HTTP 服务。两种策略:给下游也起容器(如 mock-server),或用一个 可许诺固定响应的 Stub 服务。原则是:集成测试验证"我和真实依赖的协议对不对",而不是"下游算得对不对"(那是下游自己的单测)。
| 依赖 | 推荐做法 | 原因 |
|---|---|---|
| 数据库 | Testcontainers 起真库 | 验证 SQL/事务/类型映射 |
| 消息队列 | 起内存或容器版 | 验证发布/订阅语义 |
| 下游 HTTP | WireMock 返回固定响应 | 测我的容错,不依赖对方 |
E2E 测试:让浏览器替用户点一遍
E2E 从用户视角跑完整路径(登录→加购→下单→支付),最贴近真实,但也最慢最脆。用 Playwright 之类工具驱动真实浏览器。
① 用固定等待 sleep(3) 而非等元素出现,环境一慢就红。② 选择器绑死文案/样式,改个按钮字全红,用稳定的 data-testid。③ 依赖共享账号状态,并行跑互相污染。E2E 只覆盖最核心路径,别贪多。
契约测试:消费者驱动的接口守护
微服务里"我改个字段,你挂了"是常态。契约测试让消费者定义"我需要你返回什么",提供方在 CI 里校验"我满足这个契约吗"。双方各自跑,谁破坏约定谁红,不用等联调。
论为什么微服务尤其要契约测试
服务多、团队多,单方面改接口容易让调用方线上故障。契约测试把"接口兼容性"前移到 CI:消费者说"我要这些字段"、提供方说"我给得了",任一破坏立刻失败。与 Spring Cloud Contract / Pact 对应,省去大量联调扯皮。
三类测试怎么排进一条流水线
不是所有测试每次都全跑。典型节奏:提交时跑单测(秒级)、合并前跑集成+契约(分钟级)、预发环境跑 E2E(分钟到十分钟)。这样反馈快、又不全靠慢测试兜底。
| 测试 | 触发时机 | 期望耗时 |
|---|---|---|
| 单测 + 静态 | 每次 push | < 2 分钟 |
| 集成 + 契约 | 开 PR / 合并前 | 2–10 分钟 |
| E2E | 合并后 / 预发 | 5–15 分钟 |
数据库迁移也要测:别让 schema 变更炸生产
改了模型不加迁移测试,常常"本地能跑、生产一迁移就锁表/报错"。把迁移跑在测试库上、再验证应用能正常读写,是集成层常被漏掉的一环。
删列、改类型这类"破坏性迁移"会让滚动更新期间旧 Pod 连新库直接 500。做法:分两步走(先加新列兼容、再下个版本删旧列),或设维护窗口。迁移前先在预发库演练。
提供方如何验证契约:双向才完整
消费者写完契约只是半边。提供方要在 CI 里拉取这份契约、启动自己的服务、用 Pact 校验"我确实满足它"。只有双方都绿,契约才算守住。
论契约测试的闭环
消费者说"我要这些字段"→ 提供方说"我给得了"→ 任一方改坏,对方 CI 立刻红。比"等联调环境两边都部署好才发现问题"早了整整一个周期。
性能与压测:基准、吞吐、延迟与瓶颈定位
功能对了,不代表扛得住流量。这一章讲"性能测试有哪些种类、吞吐和延迟怎么度量(P95/P99 是什么)、怎么写负载脚本、怎么定位瓶颈"。例子用 k6 与 JMH/pytest-benchmark。
性能测试的种类:别只说"压一下"
"压测"是个模糊词,不同目的用不同打法。先想清楚你要测什么。
| 种类 | 目的 | 特征 |
|---|---|---|
| 基准 Benchmark | 单请求有多快 | 极小并发、求稳定 |
| 负载 Load | 预期流量下稳不稳 | 渐进加压到目标 |
| 压力 Stress | 极限在哪、怎么挂 | 加压到崩溃 |
| 浸泡 Soak | 长时间是否泄漏 | 中负载跑几小时 |
吞吐量与延迟:P50 / P95 / P99 是什么
吞吐量(Throughput)是单位时间处理请求数(如 1000 RPS);延迟(Latency)是单个请求耗时。平均延迟会骗人——1% 的请求卡 5 秒,平均还是好看。所以看分位值:P95 表示"95% 的请求快于这个值",P99 才是长尾用户体验。
论看 P99,别只看平均
一个 API 平均 50ms、P99 却 3000ms,意味着每 100 个用户就有 1 个等到抓狂——这恰恰是客诉和流失的来源。SLA 通常按 P95/P99 写,而不是平均。
用 k6 写负载脚本
k6 用 JS 写场景,能渐进加压、测出系统在多少并发下开始劣化。下面模拟"逐步从 10 加到 100 虚拟用户"。
基准测试:函数级快慢
想知道"这个算法到底多快、改完有没有变慢",用基准工具而非手掐表。JMH(Java)和 pytest-benchmark(Python)会排除 JVM 预热、GC 噪声,给出可信数字。
瓶颈定位:先量再猜
系统慢,别急着"我觉得是数据库"。标准动作:看监控定位是 CPU / 内存 / IO / 锁哪类资源吃紧 → 抓火焰图看热点函数 → 看慢查询日志 → 定位到具体代码。每一步都要有证据。
论 profiling 的两大误区
① 凭直觉优化:花三天优化了个只占 0.1% 时间的函数。② 只看平均:平均 CPU 不高但 P99 卡,往往是 GC 停顿或锁竞争。先用火焰图/pprof 找到最宽的"柱子"再动手。
压测常见误区
① 压测环境与生产差太多:单机压出"能扛 1 万",上线还是挂。② 只看吞吐不看错误率:请求全 500 了,吞吐再高也没用。③ 没热身:JIT 没编译、连接池没建好,前几秒数字失真。④ 客户端先成瓶颈:一台机器发不出那么大压力,要分布式压测。
容量规划与拐点:何时该扩容
压测的价值不止"扛不扛得住",更在于找到拐点(knee):吞吐量加到某点后延迟陡增、错误率上升——那就是系统上限。容量规划就是"按业务峰值预留余量到拐点之前"。
| 信号 | 含义 | 动作 |
|---|---|---|
| 延迟随并发线性涨 | 还在舒适区 | 观察即可 |
| 延迟陡增、错误率抬头 | 逼近拐点 | 扩容 / 优化 |
| 吞吐不再涨甚至掉 | 已过拐点饱和 | 立即扩容 + 限流 |
论留 30% 余量
别把容量压到"刚好够峰值"。突发流量、节点故障、邻居争抢都会吃掉余量。经验上按峰值的 1.3–2 倍规划,配合 HPA(见 tech-devops 页)自动兜底。
自动化与 CI:流水线、并行、失败重试、环境
测试写一次,要每次提交都自动跑才有意义。把测试塞进 CI(GitHub Actions / GitLab CI),并处理好并行加速、flaky(偶发失败)重试、测试环境一致性。这一章给可抄的配置。
把测试塞进 CI 流水线
最小可用:每次 push/PR 自动装依赖、跑测试、覆盖率不达标就红。下面是 GitHub Actions 示意。
论CI 是"可执行的质量门禁"
把规则写进 CI,就等于让它替你盯着:谁提交让覆盖率跌破阈值、谁引入一个失败的测试,立刻红给所有人看。比"大家自觉跑测试"可靠得多。
并行与分片:让 30 分钟变 5 分钟
测试多了,串行跑会拖垮反馈速度。把用例按文件/按名字哈希分片(shard)到多个机器并行跑,再汇总。
失败重试与 flaky 处理
偶发失败(flaky)最伤信任:明明代码没病,测试却红,团队慢慢就"红也合并吧"。应对策略:先定位 flaky 根因(超时太紧、共享状态、时间戳依赖),实在短期的再有限重试,但重试次数要记下来单独追。
把 retry 当万能药,flaky 就永远修不好,还掩盖真实失败。重试只能是"止血",必须配合把 flaky 降到 0 的专项。健康的项目 flaky 率应趋近 0。
测试环境:用容器保证一致
“我本地是好的” 来自环境不一致。用 docker-compose 一键拉起应用+依赖,CI 和本地用同一份,差异归零。
质量门禁:覆盖率与复杂度阈值
门禁不止覆盖率。常见几道:单测覆盖率下限、新增代码必须覆盖、圈复杂度上限、重复率、安全扫描。都过了才放行合并。
测试数据管理:别互相踩
并行跑最怕测试间共享数据。原则:每个测试自己造数据、自己清理(或用事务回滚),或用带随机后缀的隔离命名空间,避免"A 测试删了 B 测试刚建的订单"。
论可重复 = 可信任
测试乱序、单独跑、重复跑都应得到同样结果(确定性)。做不到确定性,CI 就不可信,团队就会绕过它。
门禁之外:依赖与许可证扫描
质量门禁不只"测试过没过"。两个常被漏的:依赖漏洞扫描(你引的库有没有已知 CVE)和许可证合规(别不小心引入 GPL 这类传染型协议,法务会找你)。
论安全左移的一部分
漏洞和合规问题越晚发现越贵——上线前夜才发现用了违规协议,得连夜换库。把它们和单测一起卡在 CI 里,发现即拦截,成本最低。
测试策略与质量门禁:风险驱动、回归、度量
测试不是写完就完。这一章讲怎么定全局策略(风险驱动)、回归集怎么长、质量怎么度量、金字塔失衡有什么信号、测试左移/右移、以及用突变测试验证"测试本身有没有用"。
风险驱动测试策略
不是所有代码平等。把"出错概率 × 出错代价"画成矩阵,右上角(高频出错且代价高)重点测,左下角(工具类、几乎不变)少测甚至不测。
| 象限 | 例子 | 策略 |
|---|---|---|
| 高频错·高代价 | 支付、权限、计费 | 密集单测 + 集成 + 契约 |
| 高频错·低代价 | 表单校验 | 适度单测 |
| 低频错·高代价 | 灾备切换 | E2E + 演练 |
| 低频错·低代价 | 内部工具函数 | 少量或不测 |
论把测试预算花在刀刃上
测试也是成本。资源有限时,先补"最怕它出事"的地方。与其 100% 覆盖一个静态配置解析器,不如把精力放在"钱算对没"。
回归测试集怎么维护
每修一个 bug,就加一条测试把它钉死。回归集随项目增长——这是好事,但也要定期清理过时/重复用例,并对慢用例做分层(快集常跑、慢集定时跑)。关键是:每条回归测试都对应"一个曾真实发生过的失败"。
质量度量:覆盖率之外的指标
只盯覆盖率会跑偏。更健康的度量组合:
| 指标 | 说明 | 健康信号 |
|---|---|---|
| 覆盖率 | 代码被执行比例 | 是下限,不是目标 |
| Flaky 率 | 偶发失败占比 | 趋近 0 |
| 测试耗时 | CI 跑完多久 | 分钟级、可降 |
| 线上漏测率 | 线上 bug 中"本可被测出"的比例 | 持续下降 |
为了凑数字写 assert True 式假测试,既拖慢 CI 又制造安全感幻觉。每条测试都应能"在 bug 出现时变红"——测不出错的测试,等于没测。
金字塔失衡的信号
团队常说"我们测试很多",但其实是失衡的。几个红旗:CI 跑一次 40 分钟、E2E 经常因环境问题红、改一行要等半天、flaky 率高。说明 E2E 太多、单测太少,该把保障重心下移。
论失衡的根因常是"不好测"
单测少,往往不是人不写,而是代码耦合重、依赖直接 new 在内部、没法注入。所以"补单测"常常是"重构到可测"的契机——又回到 TDD 那句:可测即低耦合。
测试左移与右移
左移:在开发阶段就写测试、做静态分析、本地跑(越早发现问题越便宜)。右移:上线后用线上监控、影子流量、金丝雀发布继续验证(呼应 tech-devops 的可观测与发布策略)。测试不是"交付前一道关",而是贯穿全生命周期。
突变测试:测"测试本身"有没有用
覆盖率再高,也可能"跑了但没断言"。突变测试故意把代码改坏(如把 > 改成 <、把 + 改成 -),看你的测试会不会变红。不变红 = 这条测试发现不了这个 bug。
论突变测试是"测试质量的试金石"
它回答了一个覆盖率答不了的问题:"我的测试真能抓住 bug 吗?"引入成本高、慢,适合对核心模块定期跑,而不是每次 CI 都跑。
把策略写进团队约定
再好的策略不落地也是空谈。把它写进 CONTRIBUTING / 架构决策记录(ADR):单测覆盖率门槛多少、E2E 覆盖哪些路径、flaky 怎么处理、新服务必须接哪些监控。新人按文档就能对齐,评审也有据可依。
论约定优于口头默契
"大家都应该写测试"这种话没人真执行。写成可检查的规则(CI 门禁 + 文档),质量才不依赖个人自觉。测试文化不是靠倡导师傅,是靠把门槛焊进流水线。
测试用金字塔配比:大量单测(AAA 结构、TDD 红绿重构、参数化、边界与异常路径)+ 适量集成(Testcontainers 起真库)+ 少量 E2E(Playwright,只覆盖核心路径);用 Stub/Mock/Spy/Fake 隔离外部边界但别 Mock 内部实现;微服务用消费者驱动契约测试(Pact)守护接口;性能测试分清基准/负载/压力/浸泡,看 P95/P99 而非平均,用 k6/JMH 打、用火焰图定位瓶颈;把测试接入 CI,靠并行分片加速、靠重试+flaky 清单止血、靠容器统一环境、靠门禁卡质量;覆盖率只是下限,突变测试才验证测试本身有效,策略上风险驱动、左移右移结合。
1.为什么"测试金字塔"反对大量 E2E 测试?
查看答案
E2E 启动慢、依赖多、易因环境问题随机失败(脆),维护成本高。金字塔主张把保障重心放在快而廉价的单测上,E2E 只覆盖最核心的用户路径,反馈才快、才可信。
2.Mock / Stub / Spy / Fake 各有什么区别?该 Mock 什么?
查看答案
Stub 给预制答案、Mock 验证调用交互、Spy 真做事并记行为、Fake 是轻量真实实现。只 Mock 真正的外部边界(网络、DB、时间、随机),别 Mock 内部实现——否则测试只是在复述自己代码长啥样,重构就全红。
3.为什么看延迟要看 P95/P99 而不是平均值?
查看答案
平均值会掩盖长尾:1% 请求卡 5 秒,平均仍好看,但每 100 个用户就有 1 个体验极差,正是客诉来源。P99 反映最差的那批用户体验,SLA 通常按分位值写。
4.flaky(偶发失败)测试该怎么处理?无脑加重试行不行?
查看答案
不行。重试只能临时止血,会掩盖真实失败、让团队不信任 CI。正确做法:先定位根因(超时太紧、共享状态、时间依赖),把 flaky 降到 0;确实短期修不了的,单独标记、单独跑、单独追,不让它污染主流水线。
5.为什么微服务特别强调契约测试?它和集成测试有何不同?
查看答案
服务/团队多,单方改接口易让调用方线上故障。契约测试让消费者定义所需字段、提供方在 CI 校验是否满足,双方各自跑、谁破坏谁红。集成测试验证"我和真实依赖协议对不对",契约测试验证"服务间接口约定变没变",互补而非替代。
下一步往哪走
路学完测试之后
① 配 CI:去 FDE 模块 02 把测试塞进流水线,设覆盖率与 flaky 门禁。
② 串可观测:测试防"引入 bug",监控防"漏掉 bug",二者互补(FDE 模块 08 / tech-devops 页)。
③ 进阶:性能测试、混沌工程(故意制造故障)、Mutation Testing(测测试本身有效性)——本页都已点到,可深入。