4. 测试用例分层模型:测试金字塔与反模式剖析

聊到CI的用例组织,我脑子里第一个蹦出来的就是测试金字塔。这东西在微服务、模块化架构里,其实比传统单体项目更关键。为什么呢?因为你拆得越细,层与层之间的信任就越需要靠精准的测试来维系。

说实话,我见过太多团队一上来就猛写E2E,觉得“覆盖得全才放心”。结果CI跑一次四十分钟,开发改一行代码要等半天……这种体验,谁试谁知道。今天咱们就把金字塔每一层掰开看看,顺便聊聊那些常见的坑。

测试金字塔分层与反模式示意 E2E 测试 (少量关键路径) 集成测试 (模块间契约验证) 单元测试 (覆盖业务逻辑、工具函数、数据转换) ❌ 冰淇淋反模式 E2E多→单元少 ❌ 沙漏反模式 集成层缺失 理想比例:70% 单元 / 20% 集成 / 10% E2E

4.1 单元测试:守住代码的最小逻辑单元

单元测试是金字塔的底座,也是我最看重的一层。说白了,它测的是你写的那个函数、那个方法,在给定输入下能不能按预期输出。在模块化架构里,每个模块的公共函数、工具库、状态处理逻辑,都应该被单元测试先罩住。

我个人习惯:一个函数如果超过三行逻辑,或者有分支、有异常处理,那就值得写一个单元测试。你可以用 Jest、Vitest 或 JUnit 这类框架,跑起来毫秒级,CI 里几百个单元测试几秒完事。

我的经验: 有一次重构一个订单金额计算模块,单元测试覆盖率刚好85%。改完代码后跑测试,有个边界用例(折扣叠加后为负值)直接标红。要不是单元测试提前揪出来,这bug上线可就是真金白银的损失。
// 一个简单的单元测试示例(Jest)
const { calcDiscount } = require('./pricing');

test('折扣后金额不低于0', () => {
  expect(calcDiscount(10, 150)).toBe(0);     // 满100打几折?
  expect(calcDiscount(0, 100)).toBe(100);    // 无折扣
  expect(calcDiscount(20, 0)).toBe(0);       // 原价0
});

4.2 集成测试:确保模块间“握手”成功

单元测试搞定后,下一步就是看你的模块能不能和外界正常沟通。集成测试关注的是接口契约、数据库交互、消息队列调用、RPC 调用等等。它不需要真实环境全跑起来,但是要 mock 掉外部依赖的边界。

嗯,这里要特别强调:集成测试千万别写成 mini 版 E2E。我见过有些人把整个微服务启动,然后测一圈,那跟 E2E 有啥区别?集成测试应该聚焦在“当前模块对上游的依赖是否正确消费”,而不是“完整业务流程”。

测试类型关注点典型依赖速度
单元测试函数逻辑、边界条件无(纯内存)毫秒级
集成测试模块间接口、数据流转Mock Server / 嵌入式DB秒级
E2E 测试用户场景、关键链路完整环境(DB、第三方)分钟级

举个例子,你在做支付模块的集成测试,只需要 mock 外部网关的返回,验证你的业务层在“支付成功/失败/超时”三种情况下,数据库状态更新是否正确。不需要真的去扣一笔钱。

曾经踩过的坑: 我曾经在集成测试里连了真实的测试数据库,结果并行跑CI时两个job写同一张表,互相污染数据。后来切到嵌入式 H2 数据库 + flyway 初始化 schema,问题才解决。记住:集成测试的数据要隔离。

4.3 E2E 测试:只保命,不保装

E2E 是金字塔最上面那层,也是最脆弱的。因为它依赖完整环境:前端、后端、中间件、第三方服务都得在线。而且一跑就是几分钟。所以我说,E2E 只用来保住核心业务路径,不是用来覆盖所有功能的。

在模块化架构下,E2E 更适合做“冒烟检测”和“回归验证”。比如用户注册登录、下单支付这条主线。其他分支逻辑,尽量下沉到集成测试去覆盖。

我个人习惯:每个微服务只配 3~5 条 E2E,CI 里所有 E2E 加在一起不超过 20 条。多了就变成了负担,开发恨它,QA 也骂它不稳定。

避坑指南: 我曾经遇到一个项目,E2E 测试有 200 条,每次跑完平均 45 分钟,而且动不动因为超时失败。后来我们把 80% 的用例降级到集成测试,剩下 20% 最关键的保留为 E2E。CI 时间缩到 8 分钟,团队幸福感直线上升。

4.4 反模式:这些坑你最好别踩

测试金字塔本身是好东西,但很多人用着用着就跑偏了。我总结了三种常见的反模式,咱们一个一个看。

4.4.1 冰淇淋反模式

金字塔倒过来了:顶部 E2E 很大,底部的单元测试反而很少。这种情况通常出现在“测试后补”的团队,先写了 UI 自动化,后期才补单元。结果就是 CI 慢、维护成本高、定位问题难。

为什么说它反?因为你跑 E2E 失败的时候,根本不知道是哪一层出了问题。前端、后端、数据库都有可能。排查一圈下来,时间都耗光了。

  • 特征:E2E 用例数量 > 集成测试 > 单元测试
  • 后果:CI 时长失控,反馈延迟
  • 建议:逐步将 E2E 拆解为集成 + 单元,降低金字塔顶部厚度

4.4.2 沙漏反模式

单元测试和 E2E 都写了不少,但中间集成测试几乎为零。这就好比沙漏,中间细得漏不下去。你单元测试都过了,E2E 也过了,但是模块 A 调用模块 B 时参数格式没对齐——集成测试没测,只能靠 E2E 发现,浪费大量时间。

  • 特征:单元 40%、集成 5%、E2E 55%
  • 后果:接口兼容性差,现网冒泡多
  • 建议:新增模块间接口变更时,先补契约测试(Consumer-Driven Contract)

4.4.3 混乱蛋糕反模式

没有明确分层,想到哪写到哪。一个测试用例里既测了数据库,又调了第三方 API,还检查了前端界面。这种“大杂烩”测试在 CI 里是最恶心的——不可重复、不稳定、没法定位。

我曾经见过: 有个团队把注册、登录、创建订单、退款四个场景全写在一个 E2E 文件里。每次失败都不知道是哪个步骤出的问题,debug 得逐行看日志。后来我帮他们拆分成独立用例,并区分出哪些用集成测、哪些用单元测,一个月后 CI 稳定性从 60% 升到 95%。

4.5 实操建议:怎么落地金字塔

别急着理想化,我建议你按这四步走:

  1. 盘点现有测试:把你仓库里的 test/ 目录按类型统计,算一下各层占比。如果 E2E 超过 30%,就直接亮黄牌。
  2. 从最痛的地方改起:挑出 CI 里耗时最长的 3 条 E2E,问自己“这条场景能不能用集成测试覆盖?”。能的话就先拆。
  3. 约定代码规范:在 CI 中配置覆盖率门槛(单元 ≥ 60%,集成 ≥ 40%),并禁止 CI 中混用测试类型。每个测试文件在文件名上标明类型,比如 xxx.unit.test.ts、xxx.int.test.ts。
  4. 持续衡量:每两周回顾一次测试耗时、失败率、覆盖率。如果某个模块的单元测试覆盖率低于 50%,就列入技术债务。
一句话总结: 测试金字塔的核心理念不是写最多的测试,而是把正确的测试放到正确的位置。单元保逻辑,集成保接口,E2E 保关键路径。谁也别抢谁的活。