
90% 覆盖率不等于没 Bug用风险配置测试组合《工程化决策树》第 4 篇主题质量工程化。覆盖率表示代码执行过不表示关键行为验证正确。覆盖率接近九成支付回调还是出了事故我参与过一次匿名项目的高优先级事故用户已经支付订单状态却没有更新。客服电话被打爆时测试仪表盘显示的覆盖率仍接近九成。我打开支付回调的测试看到的是这些/** * 验证支付回调处理器已完成装配。 */TestDisplayName(支付回调处理器应存在)voidpaymentCallbackShouldExist(){assertNotNull(paymentCallback);}/** * 验证支付回调处理器实现了约定接口。 */TestDisplayName(支付回调处理器应实现指定接口)voidpaymentCallbackShouldImplementHandler(){assertInstanceOf(PaymentCallbackHandler.class,paymentCallback);}/** * 验证支付回调方法接收标准事件对象。 */TestDisplayName(支付回调处理方法应接收事件参数)voidpaymentCallbackShouldAcceptEvent()throwsNoSuchMethodException{assertNotNull(paymentCallback.getClass().getMethod(handle,PaymentEvent.class));}类似用例写了很多却没有一个断言“收到支付成功事件后目标订单应变为已支付”。团队测到了函数存在、类型正确、代码被调用唯独没有测业务结果。覆盖率表示哪些语句、分支或函数在测试中执行过。它不能说明断言是否有意义也不能说明最危险的场景是否被覆盖。事故暴露的不是覆盖率还不够高而是指标和风险之间断了联系。执行过不等于验证正确看一个价格函数/** * 根据数量、单价和折扣率计算价格。 */BigDecimalcalculatePrice(intquantity,BigDecimalunitPrice,BigDecimaldiscountRate){BigDecimalsubtotalunitPrice.multiply(BigDecimal.valueOf(quantity));returndiscountRate.signum()0?subtotal.multiply(BigDecimal.ONE.subtract(discountRate)):subtotal;}下面两个测试可能跑到全部分支也可能得到 100% 行覆盖率/** * 验证有折扣时会执行价格计算。 */TestDisplayName(有折扣时执行计算)voidshouldCalculatePriceWithDiscount(){calculatePrice(2,newBigDecimal(100),newBigDecimal(0.1));}/** * 验证无折扣时会执行价格计算。 */TestDisplayName(无折扣时执行计算)voidshouldCalculatePriceWithoutDiscount(){calculatePrice(2,newBigDecimal(100),BigDecimal.ZERO);}把乘法误写成减法这两个测试仍会通过。补上assertEquals(0, calculatePrice(2, new BigDecimal(100), new BigDecimal(0.1)).compareTo(new BigDecimal(180)))测试才开始验证行为。覆盖率仍然有用。它能发现从未走过的分支帮助 Review 追问遗漏也可以阻止一次提交让覆盖范围大幅倒退。但它更像扫描仪不是质量结论。团队至少还要看断言质量、缺陷逃逸和关键风险清单对高风险、纯逻辑密集的模块可以抽查变异测试验证断言是否真能杀死错误实现不必全库常态运行。测试金字塔是起点不是配额表经典测试金字塔常被画成单元 70% / 集成 20% / E2E 10%这个比例适合说明“底层测试通常更快、更便宜因此数量可以更多”不适合作为所有团队的统一目标。一个规则密集的计费库可能以单元测试为主一个集成多个 SaaS 的工作流产品需要更多契约和集成测试一个设计系统可能投入大量视觉回归遗留系统难以切开时短期内更多端到端测试也可能是现实选择。我会用四个问题决定组合而不是先分配比例失败的业务损失有多大金额、权限、数据完整性和合规风险排在展示瑕疵之前。这块代码变更有多频繁频繁修改需要更快、更稳定的反馈。最早在哪一层能发现问题能在纯函数验证的规则不必等浏览器跑完。真实环境的成本有多高数据库、消息系统、第三方沙箱和浏览器都会增加时间与维护成本。这里没有一种测试能单独覆盖所有风险类型主要回答的问题反馈与环境常见盲区单元测试规则、计算、状态转换是否正确最快环境最少发现不了装配和协议问题集成测试模块、数据库、队列能否按预期协作较快部分真实环境覆盖不了完整用户旅程契约测试服务或第三方的请求响应约定是否一致快于完整 E2E可独立运行不证明对方生产环境可用E2E真实入口到结果是否走通慢环境最接近用户定位成本高容易受环境波动影响视觉回归页面在目标视口是否出现非预期变化依赖稳定截图环境不验证业务语义测试组合的目标不是形成漂亮的金字塔而是让高损失风险至少有一条可靠、足够快的反馈路径。回到支付回调应该验证哪些风险支付回调至少涉及签名验证、金额与订单匹配、状态转换、幂等和持久化。单元测试可以覆盖状态规则集成测试用真实数据库验证写入和事务契约测试校验事件结构少量第三方沙箱测试确认签名与回调配置没有偏差。下面这条集成测试比“函数存在”更接近事故本身/** * 验证支付成功后的订单状态以及重复回调幂等性。 */TestDisplayName(支付成功后更新对应订单且重复回调保持幂等)voidshouldUpdateOrderAndKeepCallbackIdempotent(){OrderEntityorderorderFixture.create(20000L,OrderStatus.PENDING);PaymentEventeventpaymentFixture.succeeded(evt_payment_001,order.getId(),20000L);paymentCallback.handle(event);paymentCallback.handle(event);OrderEntitysavedOrderorderRepository.findById(order.getId()).orElseThrow();assertAll(()-assertEquals(OrderStatus.PAID,savedOrder.getStatus()),()-assertEquals(20000L,savedOrder.getPaidAmount()),()-assertEquals(1L,paymentRepository.countByEventId(event.eventId())));}这还不够证明支付链路万无一失。若生产配置把回调地址填错代码测试无法发现若支付平台改变签名字段本地 mock 也可能继续通过。因此要在发布前用沙箱做少量真实验证并监控生产回调积压、签名失败率和订单状态差异。“少量”也不是固定次数。沙箱昂贵或不稳定时用契约测试承担日常反馈把真实验证放在定时任务和发布门禁高风险变更则临时扩大验证范围。哪些代码不必重复测试哪些例外要保留框架与第三方库通常不需要重测 Spring MVC 如何匹配路由、ORM 如何实现基本 CRUD也不需要穷举验证库的邮箱正则。但项目对框架的配置、路由装配、过滤器顺序和升级兼容性属于自己的风险应该通过集成或冒烟测试验证。第三方调用不能只靠 mock。mock 适合制造成功、超时、拒绝等分支契约测试确保双方理解一致沙箱或测试账号用于少量验证真实签名、权限和网络行为。三者回答的问题不同。展示组件与视觉“纯展示不用测”只在视觉变化损失低、人工检查稳定时成立。结算金额、医疗提示、响应式导航、品牌组件即使业务逻辑少也可能因样式回归造成严重问题。此时视觉回归、无障碍扫描或组件快照有价值。快照只有在差异可读、评审者会认真检查时才有效。大面积自动更新快照只是把错误重新盖章。E2E 的范围“E2E 只测核心路径”是一条常用的成本控制建议不是禁令。低频但高损失的退款、账号恢复、权限撤销、数据导出也值得 E2E某些跨浏览器交互只能在真实页面暴露。反过来一个所谓核心流程如果在集成层已充分验证而 E2E 环境极不稳定可以保留少量冒烟路径把细节放到更快的层级。常见反模式比数量更值得查为门槛制造无效断言给 getter、常量或函数存在性补测试可能提升数字却没有降低失败概率。覆盖率门槛应该触发讨论不应成为奖励占位用例的 KPI。mock 掉全部边界如果 Repository、消息队列、JWT 和第三方全部按测试作者设想返回测试验证的只是内部调用剧本。至少需要一层使用真实数据库或协议实现校准 mock 与现实是否一致。测试依赖执行顺序共享userId、固定时间或同一数据库记录会把测试变成串行剧本。单个用例应能独立建立和清理数据必须共享昂贵环境时也要隔离命名空间和状态。在不同层重复同一个断言同一条折扣规则在单元、API、E2E 各穷举一遍会同时放大维护成本。更合理的做法是单元层覆盖规则边界集成层验证关键装配E2E 只验证跨系统旅程。测试文件比实现长不足以证明过度测试。复杂协议、状态机和属性测试本来可能需要更多用例。判断依据应是它是否覆盖独立风险、失败信息是否可定位、维护成本是否超过风险收益而不是文件行数。用风险选择测试的决策树这次变更最可能造成什么损失 | -- 规则算错、状态错、权限错 | -- 先用单元测试覆盖边界和反例 | -- 数据、队列或模块装配错误 | -- 用集成测试接入真实关键依赖 | -- 服务之间字段或语义不一致 | -- 用契约测试必要时补联合集成测试 | -- 用户跨页面、跨服务流程中断 | -- E2E 是否是最早能发现它的层级 | -- 是 - 添加对应旅程 | -- 否 - 放到更快、更稳定的层级 | -- 视觉、响应式或无障碍回归 | -- 视觉回归 针对性交互检查 | -- 第三方真实行为与 mock 偏离 -- 契约测试 少量沙箱验证 生产监控对每个高风险场景再确认四件事失败时会不会误报或漏报反馈能否在开发者还记得改动时返回环境成本是否可持续这个用例由谁维护。覆盖率应该怎么用MVP 阶段也不必追统一数字。先给支付、权限、金额和数据写入等高损失路径建立防线并确保测试能在日常开发中运行。产品增长后按缺陷和变更热点补集成、契约和 E2E。系统成熟后可以设置覆盖率门槛防止明显倒退但门槛要按模块风险解释生成代码和不可达防御分支也应有合理排除策略。我更愿意在 Review 里问下面这些问题这次变更可能造成的最大业务损失是什么哪个断言能直接证明关键行为正确mock 与真实依赖由哪条测试校准失败能否快速定位到规则、协议、装配或环境这套测试在三个月后还有人愿意维护吗覆盖率是参考坐标不是终点。真正有用的测试策略是在反馈速度、环境真实性和维护成本之间做选择并让选择对准业务风险。下一篇讨论发布后的恢复能力应用可以切回旧版本配置和数据却需要各自的版本与处置策略。上一篇Controller 为什么会变胖先拆职责再谈分层下一篇能部署不算本事能恢复才算把恢复能力做进发布流程《工程化决策树》第 4 篇 · lytao123