跳转到内容

测试会拖慢开发

真正拖慢开发的不是测试,而是对修改的恐惧。

这句话听起来很合理:

我们没时间写测试。

它听起来像务实,像交付压力下对重点的取舍。

但很多时候,它真正表达的是另一件事:

我们今天没时间建立安全感。
所以明天只能用手工检查、回归问题、修改焦虑和更慢的开发速度来买单。

这才是这个误区的核心。

测试不是开发结束之后额外增加的工作。好的测试本来就是开发的一部分,因为它让行为变得可以验证。

测试需要时间,缺少反馈则会让团队失去控制。

这个说法只有在短期内听起来正确

Section titled “这个说法只有在短期内听起来正确”

当然,测试需要时间。

测试不会自己出现。它需要被理解、编写、维护,有时也应该被删除。

但真正重要的问题不是:

测试花了多少时间?

更好的问题是:

如果团队不敢相信一次修改,代价是多少?

因为没有测试,并不会让这部分工作消失。它只是把工作推迟到以后。

之后要手工检查。 Code Review 时只能猜。 验收阶段再去找问题。 最后可能由客户发现 Bug。 下一次 Refactoring 时,所有人都得小心翼翼。

团队在前面省下几小时,却会在后面用不确定性、上下文切换、Regression 和越来越强的修改恐惧来偿还。

刚开始确实显得更快。

直到每一次修改都变成一场小型冒险。

测试不是目的本身。

只有当一个测试能让反馈更早、更快或者更可靠地到来,它才有价值。

没有自动化反馈时,问题往往很晚才被发现:

  • 手工测试时
  • Code Review 时
  • 验收时
  • 下一个 Sprint
  • 生产环境
  • 客户那里
  • 下一次重构时

反馈越晚,成本通常越高。

不是因为 Bug 会神奇地变大,而是因为更多上下文已经丢失,更多人被卷入,更多信任被消耗,也需要更多返工。

最昂贵的并不只是错误本身,而是太晚才被发现的错误。

这一点在架构工作中尤其明显。

Refactoring。 消除 DTO Leakage。 拆分组件。 整理 State Management。 强化模块边界。 逐步替换 Legacy Code。 重新划分接口。

这些事情听起来都很合理。

直到有人问:

我们怎么知道改完之后一切还正常?

如果答案是“我们不知道”,糟糕的架构就会被保留下来。

于是没人敢 Refactor。 大家只能继续绕着问题写代码。 组件越来越大。 Store 越来越混乱。 DTO 被继续往前端更深处传递。

不是因为没人看见问题。

而是因为没人能安全地修改它。

在这种情况下,测试不是装饰。它决定了团队是在说:

我们知道自己改了什么。

还是:

希望它别倒。

所以,“测试会拖慢开发”并不是完全错误。

它只是太粗糙了。

糟糕的测试,的确会拖慢开发。

例如:

测试 CSS Class。 测试 Private Method。 把 Framework 的偶然实现细节固定下来。 任何内部调整都会导致大量测试失败。 测试又慢、又 Flaky、又难懂。 测试存在的唯一目的,是让 Coverage 数字更漂亮。

这类测试不会建立信任,反而让团队开始不信任整个 Test Suite。

绿色 Build 也会被怀疑。 红色 Build 会被当成“估计又是 Flaky”。 每次修改都需要维护测试,却没有任何业务价值。

这不是质量保障。

这是带 Assertions 的官僚主义。

真正重要的结论是:

糟糕的测试不是反对测试的理由。
它只是反对糟糕测试的理由。

糟糕测试的替代方案不是没有测试,而是更好的测试。

好的测试保护行为,而不是实现细节

Section titled “好的测试保护行为,而不是实现细节”

好的测试并不关心代码内部此刻看起来多么优雅。

它关心的是:对外必须成立的行为是什么。

好的测试表达的是:

当这个业务场景发生时,系统必须产生这个可以观察到的结果。

糟糕的测试表达的是:

这个 Private Method 必须以这些参数被调用,虽然从业务角度没有任何人关心这件事。

两者差别非常大。

好的测试帮助 Refactoring,因为内部结构可以改变,只要外部行为保持不变。

糟糕的测试阻碍 Refactoring,因为它把实现结构误当成了行为本身。

好的测试记录例子。 糟糕的测试记录偶然实现。

好的测试降低风险。 糟糕的测试把恐惧固定下来。

Coverage 有价值。

它可以显示哪些区域完全没有被触及,暴露盲区,也可以帮助团队开始讨论。

但当 Coverage 本身成为目标时,它会变得危险。

如果目标是“80% Coverage”,团队就会优化到 80% Coverage。

却不一定优化到真正的风险保障。

于是出现一些测试:代码被执行了,但重要行为根本没有被验证。Getter 被测试,Branch 被机械地走一遍,Mock 只是在确认另一个 Mock,Snapshot 被更新时没人真正理解行为是否已经坏掉。

Coverage 能告诉你:

这段代码被执行到了。

Coverage 不能告诉你:

重要的行为得到了保护。

很多项目最终都为这个差别付出了代价。

Coverage 能说明代码是否被触及,却不能证明真正重要的行为是否得到了保护。

Test Pyramid 是一个有用的 Heuristic。

底部有大量快速测试,上层只有少量较慢、较昂贵的测试。

但 Test Pyramid 也不是自然法则。

更实用的问题是:

我们到底想保护什么风险?

清晰的逻辑通常适合快速 Unit Test。

组件行为可以通过 Component Test 来验证。

关键接口可能需要 Integration Test 或 Contract Test。

少数关键 User Journey 值得使用 E2E Test。

Legacy Code 在重构前,往往先需要 Characterization Test,把当前行为固定下来。

不同风险不需要同一种测试。

但每个关键风险,都需要某种反馈机制。

不同风险不需要同一种测试,但每个关键风险都需要反馈。

Management 正因为追求速度,才更需要测试

Section titled “Management 正因为追求速度,才更需要测试”

Management 想要速度。

这完全合理。

产品需要交付,预算有限,计划需要一定可靠性,客户也不会耐心等待团队先完成内部的“架构和谐”。

但正因为如此,测试才重要。

测试不是开发者为了欣赏干净代码而享受的奢侈品。

测试是一种机制,用来避免团队每次修改时都重新失去交付能力。

更好的 Management 问题不是:

你们为什么要写测试?

而是:

这些测试保护了什么风险?

或者:

哪种修改会因此更安全?

或者:

它们替代了哪些手工验证?

或者:

如果这个 Regression 很晚才发现,代价会是什么?

这样,讨论就不再围绕“测试数量”,而是回到风险管理。

它本来就应该在那里。

一套合理的测试策略通常很朴素

Section titled “一套合理的测试策略通常很朴素”

好的测试策略不需要宗教化。

不需要挥舞 100% Coverage。 不需要所有事情都通过 TDD 完成。 不需要每次点击都写成 E2E Test。 也不需要冻结每一个 Private Function。

它最重要的是诚实。

哪些业务规则最关键? 哪些 Integration 最危险? 哪些 Regression 会很昂贵? 哪些区域经常变化? 哪些 Legacy 区域没人敢碰? 哪些测试很慢、Flaky 或毫无价值?

从这些问题出发,可以得到一个很务实的目标:

  • 识别风险
  • 收集业务示例
  • 用快速测试保护关键逻辑
  • 有针对性地测试危险 Integration
  • 在 Refactoring 前记录 Legacy 行为
  • 把 E2E Test 集中在少数关键 Flow
  • 修复或者删除 Flaky Test
  • 把 Coverage 当成诊断,而不是目标

这没有 Test Dogma 那么壮观。

但有用得多。

测试不会自动让开发变快,但它让安全的速度成为可能。

“测试会拖慢开发”太粗略了。

更准确的说法是:

糟糕的测试会拖慢开发。
好的测试会限制风险。
没有测试,只是把成本推迟到未来。

或者再短一点:

测试不是刹车。
糟糕的测试才是刹车。
没有测试是一笔贷款。
而每一次对修改的恐惧,都是在付利息。