TESTING · 质量保障 / 缺陷防线
软件测试速查
从测试金字塔到用例设计方法,从 JUnit 5 与 Mockito 的常用姿势到接口测试要点、Bug 全生命周期,质量保障的 68 个高频知识点一页收齐。
68条速查
12大主题
∞持续更新
📖 速查表
点击展开各小节
🔺 测试金字塔
| 层级 | 说明 | 要点 |
|---|---|---|
| 单元测试 | 金字塔底层,验证单个类或函数的逻辑正确性 | 数量占比约 70% 毫秒级反馈,可随构建频繁运行 |
| 接口 / 集成测试 | 验证模块间协作:Service + DB + 中间件的组装是否正确 | 占比约 20%;可借助 Testcontainers、H2 模拟依赖 |
| 端到端(E2E) | 模拟真实用户走完整业务流程,最贴近线上真实场景 | 占比约 10% 慢且脆弱,只覆盖核心链路 |
| 比例经验值 | 单元 : 接口 : E2E ≈ 7 : 2 : 1,越往上越慢、越贵、越难定位问题 | 7 : 2 : 1 经典模型 |
| 反金字塔警示 | 只写 UI 层自动化的「冰淇淋筒」结构:不稳定、维护成本高 | 缺陷发现晚、修复成本高,应把覆盖重心下移到底层 |
🗂️ 测试分类
| 维度 | 分类 | 说明与要点 |
|---|---|---|
| 按可见性 | 白盒测试 | 基于代码内部结构设计用例,追求语句、分支、条件覆盖;单测阶段常用 |
| 按可见性 | 黑盒测试 | 只关注输入与输出,不关心内部实现;功能验收常用 |
| 按可见性 | 灰盒测试 | 结合白盒与黑盒:从外部验证,同时参考部分内部信息;接口测试多属此类 |
| 按质量属性 | 功能 / 性能 / 安全 / 兼容 | 性能关注 TPS、P99 延迟与资源占用;兼容覆盖浏览器、系统、分辨率组合 |
| 按执行时机 | 冒烟 / 回归 / 探索性 | 冒烟:验证版本是否可测;回归:改动后确认老功能未坏;探索性:凭经验自由挖掘缺陷 先冒烟后细测 |
| 按执行方式 | 手工 vs 自动化 | 重复执行多、回归量大的场景优先自动化;一次性与探索性场景适合手工 |
🎯 用例设计方法
先等价类与边界值打底,再用判定表与场景法补组合与流程,最后错误推测兜底。
| 方法 | 要点 | 示例 |
|---|---|---|
| 等价类划分 | 把输入划分为有效 / 无效等价类,每类取一个代表值,大幅缩减用例数 | 年龄 1~120:有效取 25;无效取 0、200、"abc" |
| 边界值分析 | 缺陷常藏在边界:取 min、min+1、正常值、max-1、max 五点 | 分页 pageSize 取 1、100、101;金额取 0、0.01 |
| 判定表 | 多条件组合时列出条件与动作的对照表,保证组合覆盖不遗漏 | 满减规则:会员 × 金额档位共 4 种组合 |
| 场景法 | 按用户操作路径设计基本流 + 备选流,覆盖完整业务旅程 | 下单:正常支付 / 超时取消 / 库存不足 |
| 错误推测 | 凭经验直击系统易错点,补充前述方法难以覆盖的情形 | 必填项为空、特殊字符、重复提交、弱网断连 |
| 正交试验法 | 参数组合爆炸时用正交表抽样,以最少用例实现均匀覆盖 | 兼容性:浏览器 × 系统 × 分辨率 省用例 |
☕ JUnit 5 常用注解
| 注解 | 说明 | 示例 |
|---|---|---|
| @Test | 标记测试方法,JUnit 5 中方法无需 public 修饰 | @Test void shouldAdd() {} |
| @DisplayName | 为测试类 / 方法定义可读名称,报告里一目了然 | @DisplayName("下单成功生成订单号") |
| @BeforeEach / @AfterEach | 每个测试方法执行前后各运行一次,用于准备与清理数据 | 初始化 Mockito、清理测试表数据 |
| @BeforeAll / @AfterAll | 整个类只执行一次,默认必须 static(或改用 @TestInstance 生命周期)易错 | 启动 / 关闭昂贵资源:容器、连接池 |
| @Disabled | 暂时跳过测试,务必注明原因避免被遗忘 | @Disabled("等待修复 BUG-123") 注明原因 |
| @ParameterizedTest | 参数化执行:同一逻辑跑多组数据,配合 @ValueSource、@CsvSource、@MethodSource | @CsvSource({"1,1,2", "2,3,5"}) |
| @RepeatedTest | 重复执行 N 次,用于验证幂等性或偶发的随机问题 | @RepeatedTest(10) |
| Assertions 断言 | assertEquals / assertTrue / assertThrows 等,异常断言验证抛错路径 | assertThrows(IllegalArgumentException.class, () -> svc.pay(order)) |
🎭 Mockito 常用
| 用法 | 说明 | 示例 |
|---|---|---|
| mock() / @Mock | 创建对象替身,屏蔽数据库、网络等外部依赖,让单测只聚焦被测逻辑 | @Mock UserRepository userRepository; |
| when - thenReturn | 打桩:指定 mock 对象某次调用的返回值 | when(repo.findById(1L)).thenReturn(Optional.of(user)) |
| @InjectMocks | 创建被测对象,并把 @Mock 依赖自动注入进去 | @InjectMocks UserService userService; |
| verify | 验证交互行为:方法是否被调用、调用了几次 | verify(repo, times(1)).save(any()) 行为验证 |
| 参数匹配器 | any() / eq() / anyLong();规则:混用时所有参数都必须用匹配器 | verify(repo).delete(eq(1L)) |
| ArgumentCaptor | 捕获传入 mock 的实际参数,对参数内容做精细断言 | captor.getValue().getStatus() |
📡 接口测试
| 名称 | 说明 | 示例 / 要点 |
|---|---|---|
| Postman 环境变量 | 为 dev / test / prod 建立环境集,一套脚本多环境切换 | {{base_url}}/api/users |
| Postman 断言 | 在 Tests 标签编写脚本断言状态码与响应字段 | pm.response.to.have.status(200) |
| Collection Runner | 批量执行集合用例,支持数据文件驱动,适合批量回归 | CSV / JSON 参数化批量跑 数据驱动 |
| cURL -X / -H | -X 指定请求方法,-H 添加请求头 | curl -X POST -H "Content-Type: application/json" ... |
| cURL -d / -i | -d 发送请求体,-i 在响应中附带响应头 | curl -i -d '{"name":"a"}' url |
| cURL -k / -v | -k 跳过证书校验,-v 输出详细请求响应过程 | 调试 HTTPS、排查握手与重定向问题 |
| 测试关注点 | 状态码语义正确、响应结构与字段断言、鉴权(401/403)、幂等性 | POST 重复调用不应重复下单 幂等必测 |
🐞 Bug 生命周期
| 环节 / 概念 | 说明 | 要点 |
|---|---|---|
| 状态流转 | 新建 → 确认(指派)→ 修复 → 验证 → 关闭 | 验证不通过则重开(Reopen)闭环 |
| 分支处理 | 拒绝(非缺陷 / 无法复现)、延期(低优先级暂缓)、重开(验证失败) | 拒绝必须给出理由并与提出人沟通对齐 |
| 严重级别 | 衡量缺陷影响程度:致命 > 严重 > 一般 > 轻微 | 崩溃、数据丢失属致命;错别字属轻微 |
| 优先级 | 衡量处理紧迫度:P0 立即 > P1 高 > P2 中 > P3 低,由业务排期决定 | 严重 ≠ 优先级:登录页错别字也可能 P0 双维度独立 |
| 好报告要素 | 一句话标题、可复现步骤、预期 / 实际结果、环境版本、日志与截图 | 「偶现」类问题附出现概率与录屏 可复现第一 |
| 回归验证 | 修复后按原步骤复测,并检查关联功能未被波及 | 回归通过才可关闭,附验证结果 |
🔄 TDD 与 BDD
| 名称 | 说明 | 要点 |
|---|---|---|
| TDD 红-绿-重构 | 先写一个失败的测试(红),写最少代码让其通过(绿),再重构消除重复,小步循环推进 | 循环以分钟计,每次只前进一小步 红-绿-重构 |
| TDD 常见误区 | 先写完全部实现再补测试是「补测试」不是 TDD;测试也并非越多越好,关键在先于实现 | 写测试的过程就是澄清接口设计的过程 测试即设计 |
| TDD 适用场景 | 逻辑清晰、接口稳定的业务规则与纯函数最适合;探索性 UI 与胶水代码收益较低 | 需求频繁变更的场景收益最大:回归安全网价值突出 |
| BDD 三段式 | Given 前置条件、When 触发动作、Then 预期结果,用业务语言描述场景,各方共用一套表达 | Given 余额 100 When 取款 80 Then 余额 20 |
| BDD 工具与实践 | Cucumber、JBehave 把场景文本映射为可执行步骤,验收标准即自动化用例 | 产品 / 测试 / 开发共建场景,写业务语言不写实现细节 |
| TDD vs BDD 选型 | TDD 聚焦单元级正确性,BDD 聚焦业务行为验收,二者互补而非替代 | 外层 BDD 定验收、内层 TDD 保实现 由外而内 |
🤖 自动化策略
| 名称 | 说明 | 要点 |
|---|---|---|
| 适合自动化 | 需求稳定、执行频繁、结果可判定的场景:回归验证、冒烟检查、接口与数据校验 | 一次性探索性需求保留手工,别为自动化而自动化 |
| 回归集分层 | 核心冒烟集随提交分钟级全量跑,全量回归集夜间或触发式执行,快慢分离 | 核心集控制在 10~15 分钟 内,保证每次提交可跑 分层执行 |
| 用例准入标准 | 进入回归集的用例须过评审:步骤可复现、断言明确、无强环境依赖 | 新用例上线前至少连续通过多次再纳入,防天生不稳 |
| Flaky 治理 · 隔离 | 不稳定用例先打标签移出主干回归集,防止「狼来了」拖垮整条流水线信任 | 隔离 ≠ 删除:挂看板跟踪,限期修复或废弃 先隔离再修 |
| Flaky 治理 · 定位 | 四类根因排查:测试数据冲突、环境抖动、异步等待不足、用例间耦合 | 用重跑日志与录像定位首错点,修复后观察期通过再回主干 |
| 重试的正确用法 | 失败自动重试只作缓冲手段,且每次重试必须记录进报告 | 重试掩盖的是症状不是根因,重试率升高说明有系统性问题 治标更治本 |
✅ 断言与数据构造
| 名称 | 说明 | 要点 |
|---|---|---|
| AssertJ 链式断言 | 流式 API 可读性远超原生 JUnit:assertThat(list).hasSize(3).contains(...) | 失败信息自带实际值与差异,排查更快 可读性优先 |
| AssertJ 常用姿势 | 集合用 extracting / filteredOn,异常用 assertThatThrownBy,软断言 SoftAssertions 收集全部失败 | 一条断言验证一个关注点,失败时定位最直接 |
| 数据构造 · Builder | 用 Builder / 工厂方法封装「有效默认值」,测试里只覆盖与场景相关的字段 | aUser().withBalance(0).build(),新增字段不破坏全量用例 屏蔽无关字段 |
| 数据构造 · 固定种子 | 随机数据用固定种子的 Random 与固定时钟,保证失败可复现 | new Random(42) + 注入 Clock.fixed(...),失败可重放 |
| 清理 · 事务回滚 | 单测加 @Transactional,每个用例结束自动回滚,互不污染 | 注意 REQUIRES_NEW 等已提交的数据不会回滚,需另行清理 |
| 清理 · @Sql 与前后置 | 复杂初始化用 @Sql 执行造数脚本,@AfterEach 或清理 SQL 兜底 | 先删子表再删父表;共享基础数据用「重建」代替「删除」 用例隔离 |
🧱 单测标准结构
一个可直接照抄的 JUnit 5 + AssertJ 标准单测骨架:Given-When-Then 三段式、@Nested 按场景分组、断言链验证业务结果——照此写即可通过用例评审。
@DisplayName("订单支付")
class OrderPayTest {
@Mock AccountClient accountClient;
@Mock PaymentGateway paymentGateway;
@InjectMocks OrderService orderService;
@Nested
@DisplayName("余额充足场景")
class BalanceEnough {
@Test
@DisplayName("支付成功,订单流转为已支付")
void pay_success() {
// Given 前置:造一个金额 99 的订单,账户余额打桩为 100
Order order = anOrder().withAmount(new BigDecimal("99.00")).build();
when(accountClient.getBalance(UID)).thenReturn(new BigDecimal("100.00"));
// When 动作:只触发一次被测行为
PayResult result = orderService.pay(order, UID);
// Then 断言:验证业务结果而非实现细节,链式写法失败时自动打出期望与实际差异
assertThat(result.isSuccess())
.as("支付应成功")
.isTrue();
assertThat(order.getStatus())
.as("订单应流转为已支付")
.isEqualTo(OrderStatus.PAID);
assertThat(result.getBalanceAfter())
.as("余额应扣减为 1")
.isEqualByComparingTo("1.00");
// 交互断言兜底:支付网关恰好调用一次,不多不少
verify(paymentGateway, times(1)).charge(any());
}
}
}
🐞 好用例的五个标准
| 标准 | 判定方法 | 自检要点 |
|---|---|---|
| 失败即定位 | 挂了能一眼看出哪坏了:断言直接指向业务结果,失败信息自带期望值与实际值,而不是一条裸 assertTrue(false) | AssertJ 的 as("…") 业务描述不可省 一眼定位 |
| 不依赖顺序 | 单独跑、全量跑、乱序跑、并行跑结果都一致,用例之间零共享状态 | 删掉或调换任意用例后是否仍全绿?独立可并行 |
| 不依赖环境 | 不连外网、不依赖特定测试账号与真实数据库,外部依赖一律 mock | 断网或换台机器还能跑吗?依赖全 mock |
| 断言业务结果而非实现 | 验证「订单变为已支付」而不是「调用了某个私有方法」,实现重构时用例不红 | 重构实现后用例是否依然成立?抗重构 |
| 跑一百次结果一致 | @RepeatedTest(100) 无随机失败才配进回归集;偶现失败必须查到根因 | 出现一次「偶现」就盯到定位为止 拒绝 flaky |