TESTING · 质量保障 / 缺陷防线

软件测试速查

从测试金字塔到用例设计方法,从 JUnit 5 与 Mockito 的常用姿势到接口测试要点、Bug 全生命周期,质量保障的 68 个高频知识点一页收齐。

68条速查 12大主题 持续更新

📖 速查表

点击展开各小节

🔺 测试金字塔
层级说明要点
单元测试 金字塔底层,验证单个类或函数的逻辑正确性 数量占比约 70% 毫秒级反馈,可随构建频繁运行
接口 / 集成测试 验证模块间协作:Service + DB + 中间件的组装是否正确 占比约 20%;可借助 Testcontainers、H2 模拟依赖
端到端(E2E) 模拟真实用户走完整业务流程,最贴近线上真实场景 占比约 10% 慢且脆弱,只覆盖核心链路
比例经验值 单元 : 接口 : E2E ≈ 7 : 2 : 1,越往上越慢、越贵、越难定位问题 7 : 2 : 1 经典模型
反金字塔警示 只写 UI 层自动化的「冰淇淋筒」结构:不稳定、维护成本高 缺陷发现晚、修复成本高,应把覆盖重心下移到底层
测试金字塔
数量比:单元 60~70% / 接口 20~30% / UI 10%,越往上越慢越脆
🗂️ 测试分类
维度分类说明与要点
按可见性 白盒测试 基于代码内部结构设计用例,追求语句、分支、条件覆盖;单测阶段常用
按可见性 黑盒测试 只关注输入与输出,不关心内部实现;功能验收常用
按可见性 灰盒测试 结合白盒与黑盒:从外部验证,同时参考部分内部信息;接口测试多属此类
按质量属性 功能 / 性能 / 安全 / 兼容 性能关注 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