测试期望值应该来自与实现相同的地方吗?

发布: (2026年2月9日 GMT+8 01:44)
11 分钟阅读
原文: Dev.to

Source: Dev.to

介绍

这里有两个测试验证相同的行为:

test('calculates tax correctly', () => {
  const price = 1000;
  const tax = calculateTax(price);
  expect(tax).toBe(100);
});
import { TAX_RATE } from './config';

test('calculates tax correctly', () => {
  const price = 1000;
  const tax = calculateTax(price);
  expect(tax).toBe(price * TAX_RATE);
});

两个测试检查的是同一件事,但期望值来自完全不同的地方。
第一个硬编码了 100;第二个从配置中导入。这个差异重要吗?

深入研究后,我产生了一个更根本的问题:测试的期望值应该来源于何处。我并不确定自己已经得到正确答案,但希望这至少能提供一些思考的素材。

测试Oracle的概念

1978年,William E. Howden 引入了 test oracle(测试Oracle)的概念。他在 1981 年的出版物中更清晰地阐述了这一思想:

使用测试需要存在一种外部机制,用于检查测试输出的正确性。该机制被称为测试Oracle。

这里的 external(外部)似乎是关键。用于判断测试正确性的信息必须来自 程序本身之外的东西

随后,这一思想在多个方向上得到了发展:Elaine Weyuker 对“non‑testable”(不可测试)程序的定义、Cem Kaner 将 Oracle 问题视为软件测试教育中决定性挑战之一的强调,以及 James Bach 和 Michael Bolton 的 oracle‑consistency 启发式等。

尤其是 Bolton,认为 Oracle 的目的不是 证明 正确性,而是 检测 问题。这个区分对当前的问题尤为相关。

从配置导入的测试

让我们仔细看看介绍中的第二个版本——从 config 导入的那个:

import { TAX_RATE } from './config';

test('calculates tax correctly', () => {
  const price = 1000;
  const tax = calculateTax(price);
  expect(tax).toBe(price * TAX_RATE);
});

这个测试看起来很合理。它遵循了 DRY 原则,并且如果数值发生变化,只需要在一个地方更新即可。

然而,从 Howden 原则的角度来看,这个测试可能存在问题。期望值来自与实现相同的 config。如果有人不小心把 TAX_RATE 改成 0.01,测试仍会通过。该测试已经成为 实现的镜像——而镜子会反射出与原物相同的失真。

Source:

硬编码能解决问题吗?

现在考虑第一个版本——即硬编码 100 的那个:

test('calculates tax correctly', () => {
  const price = 1000;
  const tax = calculateTax(price);
  expect(tax).toBe(100); // assuming 10% tax rate
});

通过硬编码 100,期望值变得独立于实现。如果 TAX_RATE 被意外修改,测试会失败并捕获问题。

但这会引入另一个问题。100 是硬编码的数值,当 TAX_RATE 有意 改变时,测试也会失败。这并不是“问题检测”——它只是测试维护上的疏忽。

此外,如果 100 背后的推理仅仅是“配置文件里写的是 0.1,所以 1000 × 0.1 = 100”,那么你实际上是手动复制了配置值。这本质上等同于复制一个硬编码常量。

另一方面,如果推理是“法律规定的税率是 10 %”,那么期望值基于一个外部事实,它独立于实现——正是 Howden 所描述的“外部机制”。

换句话说,即使对于同样的硬编码 100,其含义也会因是否能够在不查看代码的情况下解释期望值而不同。这似乎是区分重复硬编码和独立预言机的界限。可能还有更好的标准,但这就是我在研究中得出的结论。

重新审视与 DRY 的关系

概括至今的情形:

  • 从配置中导入遵循 DRY。
  • 硬编码期望值似乎违背了 DRY。

遵循 DRY 会削弱 oracle(预言机)独立性;而保持 oracle 独立性似乎又会违背 DRY。这两条原则看似冲突。

然而,当我们回到 DRY 的原始定义时,它们可能根本不冲突。

DRY 关注的是知识、意图的重复。
Andy Hunt & Dave Thomas,《The Pragmatic Programmer》(20 周年版)

在题为“并非所有代码重复都是知识重复”的章节中,他们解释说,即使验证年龄的代码与验证数量的代码完全相同,这两种验证仍代表不同的知识。这是一种巧合,而不是 DRY 违规。

将此应用到测试期望值上:

  • 生产代码 TAX_RATE = 0.1 表示的知识是“该系统使用的税率为 0.1”。
  • 测试 expect(tax).toBe(100) 表示的知识是“在 10 % 税率下,1000 的税额应为 100”。

这两个地方恰好使用了相同的数值,但它们表达的是不同的知识。前者是系统配置,后者是业务规则的验证。如果严格遵循《The Pragmatic Programmer》中的定义,在测试中硬编码 0.1(或 100并不是 DRY 违规——它只是“不同的知识恰好共享相同的值”。

换句话说,DRY 与 oracle 独立性实际上可能并不冲突。

冲突:回到 DRY 原始定义时

当 DRY 被 狭义解释 为“消除代码重复”时,才会出现表面的冲突。

我不能确定这种解释是否正确。但依据最初的定义,把硬编码的测试期望值视为 DRY 违规似乎有点牵强。

当共享配置值变得危险时

让我们回到配置共享的问题。即使硬编码是有理由的,也有一个更结构性的问题值得考虑。

当系统中的每个单元都引用同一个配置常量时,配置中的错误值会传播到所有依赖它的单元。如果测试也引用同一个配置,那么 没有人——甚至连测试也——能捕捉到这个错误。这种风险与系统是单体还是微服务架构无关。它是依赖单一真相来源的固有风险。

从单元层面来思考,只要每个单元的测试把输入值视为“假设正确”,并原样使用,那么这些值的错误会在每一层测试中传播而不被发现。这可能是一个相当强的断言,但从逻辑上讲应该成立。

测试属性而非精确值

为每个配置值准备独立的硬编码期望值并不实际。税率有法律依据,但重试次数呢?通常没有外部权威规定“这个应该是 3”。对于这类值,找到独立的预言机确实很困难。

一种可能有帮助的方法是 测试属性和关系,而不是精确值

test('retry count is within a reasonable range', () => {
  expect(MAX_RETRY).toBeGreaterThan(0);
  expect(MAX_RETRY).toBeLessThanOrEqual(10);
});

test('retry count and timeout are consistent', () => {
  expect(MAX_RETRY * RETRY_INTERVAL).toBeLessThanOrEqual(TIMEOUT);
});

这类测试在值变化时不需要更新。它们只捕获 意外 的破坏。而且因为这些约束基于设计意图——作为外部依据——它们看起来与 Howden 原则是一致的。

归根结底这是一种权衡

Howden 原则在理论上是站得住脚的。所有期望值都应独立于实现的论点在逻辑上是正确的。但对 每一个 配置值进行独立验证并不现实。

最重要的是 你是否意识到自己在做权衡。在“这个值理想情况下应该独立验证,但我们出于有意识的成本‑收益考量而从配置中共享”与“毫无思考地直接共享”之间,有着显著的区别。

回到最初的问题:期望值是硬编码还是从配置导入重要吗?我认为重要——但不是因为某一种方法在所有情况下都更好。关键在于选择是否是经过深思熟虑的,并且了解所获得的收益与所失去的东西。

也许测试的价值并不取决于其形式,而是取决于背后判断的质量。我可能并不完全正确,但这是我通过这次研究得出的结论。

参考文献

  • William E. Howden, “程序测试的理论与实证研究”, IEEE Transactions on Software Engineering, 1978
  • William E. Howden, “动态分析方法综述”, 收录于 Software Validation and Testing Techniques, IEEE Computer Society, 1981
  • Andy Hunt & Dave Thomas, 《实用程序员:通往精通之路,20周年纪念版》, Addison‑Wesley, 2019
  • Earl T. Barr, Mark Harman, Phil McMinn, Muzammil Shahbaz, Shin Yoo, “软件测试中的 Oracle 问题:综述”, IEEE Transactions on Software Engineering, 2015
  • Oracle 问题 – YLD Blog
  • Oracle 问题与软件测试教学 – Cem Kaner
  • Oracle 关注的是问题,而非正确性 – DevelopSense
  • DRY 原则与偶然重复 – Anthony Sciamanna
0 浏览
Back to Blog

相关文章

阅读更多 »

停止过度工程化(2025)

为什么你的“Professional” Architecture正在扼杀你的Startup Professionalism Paradox 大多数developers并不是因为缺乏技术技能而失败;他们是因为…

Show HN: 音乐音程训练器

Show HN:Musical Interval Trainer - 文章链接:https://valtterimaja.github.io/musical-interval-trainer/ - 评论链接:https://news.ycombinator.com/item?id=...