2011年2月22日,Google Testing Blog 发了一篇题为《This Code is CRAP》的文章。作者是当时在 Google 推动敏捷与开发者测试的 Alberto Savoia。文章评论区迅速涌入大量求助与吐槽:官方提供的 Eclipse 插件下载地址全数失效,更新站点抛出 Gateway Timeout,下载包里直接缺失关键的 feature.xml 1.1.6 描述文件;随着 Java 7 普及,原生工具更是在新字节码环境下直接崩溃。

一个被 Google 工程师郑重推介、试图挽救软件工程质量的核心工具,在发布短短几年内就陷入了生态停摆。开发者们在废墟般的评论区里寻找民间的补丁链接,有人甚至疑惑:这个被冠以 CRAP 之名的天才度量体系,怎么连一篇正规的同行评审学术论文都没有?

这场十多年前的断代公案,撕开了一道至今仍在流血的行业伤口:虚荣指标正在毁掉工程质量

CRAP 评价体系的双重警戒线 危险代码临界点 CRAP ≥ 30 高圈复杂度叠加低覆盖率,判定为 Crappy 单测救赎绝望线 Comp ≥ 31 即使达到 100% 测试覆盖率,得分依然 ≥ 31

解剖 CRAP:当圈复杂度越过 31 的死线

要理解开发者的惋惜,得先看懂 Savoia 到底造了什么。在敏捷开发早期,圈复杂度(Cyclomatic Complexity)和代码覆盖率是两套割裂的语言:复杂度高说明逻辑分支错综复杂,覆盖率低说明缺乏安全网。

当圈复杂度超过31后,即使全量单测也无法赦免代码死结(示意图)
当圈复杂度超过31后,即使全量单测也无法赦免代码死结(示意图)

2007年,Alberto Savoia 与 Bob Evans 在技术网站 Artima 的专栏连载中提出了复合度量指标 CRAP,全称是 Change Risk Anti-Patterns。它并不是书斋里的学术推导,而是一个非线性惩罚经验公式

CRAP(m) = comp(m)^2 * (1 - cov(m)/100)^3 + comp(m)

公式把方法的圈复杂度标记为 comp(m),测试覆盖率标记为 cov(m)。在这套数学结构里,开发者对复杂度的放纵会遭到三次方级别的惩罚。CRAP 把 30 分 划为危险红线,一旦越过,该方法就被标记为必须治理的烂代码。

更致命的设计藏在后面。如果一个方法的圈复杂度达到 31,代入公式计算,哪怕开发者编写了极其严密的单元测试把覆盖率刷到 100%,后面立方的惩罚项变成零,残存的基底复杂度依然让得分锁定在 31。

圈复杂度越过 31 后,任何测试都无法再为系统提供赦免。

设计者的潜台词清晰且刻薄:逻辑盘根错节到这个地步,写单测只是在屎山上雕花。唯一的生路不是写测试,而是强制拆分与重构。

工程质量度量的路线分裂 传统行覆盖率(Line Coverage) 制造伪测试的 KPI 陷阱 • 诱使研发为简单 getter/setter 编写单测 • 学术证明大量执行代码缺乏有效断言 • 达到 80% 覆盖率却依然漏洞频发 CRAP 复合分析模型 非线性压制架构劣质度 • 结合分支圈复杂度实施高次惩罚 • 强制复杂方法必须提供顶格保护 • 倒逼架构拆分,杜绝单一巨型函数

覆盖率拜物教与伪测试的溃败

这种激进的约束模型,在当年的工程现场击中了一线开发者的痛点。主流覆盖率工具(如早期的 CodePro 或后来的 JaCoCo)给团队定下的 80% 考核硬指标,本质上是管理层的偷懒。

考核指标催生伪测试,看似严密的覆盖率防线内部完全悬空(示意图)
考核指标催生伪测试,看似严密的覆盖率防线内部完全悬空(示意图)

为了应付流水线门禁,工程师不得不花费大量精力去给 public Value getValue() { return value; } 这样的平庸方法补测试用例。学术界的实证研究早已揭露了这种形式主义的代价:单纯追求覆盖率会催生海量的伪测试(pseudo-tested methods)。这些用例确实把代码行执行了一遍,但没有包含任何有效的业务断言,根本不具备故障拦截能力。

古德哈特定律在软件测试领域再次应验:当覆盖率变成考核指标时,它就不再是一个好的度量工具。

CRAP4J 之所以让当年的严肃工程师着迷,是因为它提供了一张清醒的热点地图。它不要求开发者给每行安全代码都上一把锁,而是把火力集中在缺乏保护的复杂雷区。它尊重常识,试图把开发者从指标考核的无聊苦工中解放出来。


基础设施决裂与安全暗角的代价

然而,一个充满洞见的工程指标,为何在现实中败得如此彻底?答案藏在基础设施维护者的技术洁癖与开源项目的荒废里。

官方生态枯萎域名停摆,残留在流水线的老旧组件反成安全隐患
官方生态枯萎域名停摆,残留在流水线的老旧组件反成安全隐患

作为 Java 覆盖率事实标准的统治级引擎,JaCoCo 团队在官方 GitHub Issue #196 中,明确将集成 CRAP 指标的诉求关闭为 wontfix。维护者的理由相当严谨:Savoia 的原型依赖基路径覆盖率,而现代字节码引擎普遍基于分支或指令覆盖;加之 CRAP 公式带有极强的经验拟合与主观加权性质,无法作为底层元数据标准直接推行。

被主流引擎拒之门外后,CRAP 相关的周边生态急速枯萎。CRAP4J 1.1.6 插件的官方更新站点 junitfactory.com 与 crap4j.org 域名相继死掉,代码库长期无人维护。更讽刺的是,许多持续集成流水线里残留的陈旧工具,最终变成了系统的安全炸药。

  • 风险.Jenkins 官方归档的 crap4j-plugin 最终版本停留在 0.9,其 GitHub 仓库永远停在了未发布的 0.10-SNAPSHOT。该插件潜藏未修补的 CVE-2023-28680 高危 XXE 漏洞。如果现代 CI 环境盲目部署老旧归档工具,非但换不来质量提升,反而会直接向攻击者敞开内网大门。
  • 结论.试图用任何单一复合指标替代工程师的架构直觉,都是危险的。如果盲目将 CRAP 设为硬性考核门禁,团队很可能为了达标而强行把完整的业务逻辑切碎成几十个无意义的微小函数,最终系统圈复杂度降下来了,整体可读性却荡然无存。

回到当下,在 Java 17 和 Java 21 的现代研发体系中,对抗覆盖率形式主义的武器已经迭代。开发者有了变异测试(Mutation Testing),社区也衍生出了适配新运行时的 open-crap4j。

当年 Alberto Savoia 掀起的讨论并没有过时。度量工具的使命是照亮暗角,而不是成为代码的监牢。