权衡:干净的测试 vs. 代码简洁性(现代 JS)

发布: (2026年1月20日 GMT+8 01:57)
7 min read
原文: Dev.to

Source: Dev.to

大家好,开发者们!👋

在现代的 JavaScript 和 TypeScript 开发中,我们不断在两股相对的力量之间取得平衡:

  • 代码简洁 – 编写简洁、最小化的代码。
  • 清晰测试 – 编写易于隔离和验证的代码。

通常,写得最快的代码也是最难测试的。相反,为可测试性而设计的代码乍一看往往显得“模板繁重”。

下面我们将通过真实案例探讨这种权衡,从常见模式出发,逐步进入构建长期可扩展系统所需的思维方式。

第 1 轮:环境变量困境

在代码审查中常见的经典争论:如何访问由 Vite、Webpack 或 Node 提供的环境变量(API 密钥、功能标记等)?

“简洁”方式(静态常量)

最快的办法是直接读取变量并存入常量。只有一行代码,简单明了。

// config.ts
export const IS_PRODUCTION = import.meta.env.PROD;
export const API_URL = import.meta.env.VITE_API_URL;

// myFeature.ts
import { IS_PRODUCTION } from './config';
if (IS_PRODUCTION) {
  // 做可怕的真实操作
}

隐藏的代价

  • 代码与构建系统的全局状态紧密耦合。
  • 当你对 myFeature.ts 进行单元测试时,IS_PRODUCTION 会在测试文件加载时 立即 求值。
  • 一旦常量被设为 truefalse,在同一次测试运行中几乎不可能再更改它。

为了同时测试两种情形,你常常需要“全局存根”,例如让 Vitest 或 Jest 改变运行时环境:

// ❌ 乱七八糟的全局测试
vi.stubEnv('PROD', 'true'); // 现在所有测试都认为是生产环境
// …如果忘记取消存根,其他测试会莫名其妙地出错

“可测试”方式(Getter 函数)

把访问封装在函数中。虽然会多一点样板代码,却能创建一个干净的 seam(接缝)。

// config.ts
export const getIsProduction = () => import.meta.env.PROD;

// myFeature.ts
import { getIsProduction } from './config';
if (getIsProduction()) {
  // 做可怕的真实操作
}

好处:创建 Seam

Seam(由 Michael Feathers 推广)是指可以在不修改源代码的情况下改变程序行为的地方。在我们的测试中不再需要去 hack 全局环境;只需对普通函数进行间谍(spy)即可。

// ✅ 干净的隔离测试
import * as Config from './config';

test('仅在生产环境执行可怕操作', () => {
  const spy = vi.spyOn(Config, 'getIsProduction');

  spy.mockReturnValue(true);
  // 对生产环境的期望...

  spy.mockReturnValue(false);
  // 对非生产环境的期望...
});

可测试的做法以少量额外字符换来了隔离性和可控性。

Source:

第 2 轮:处理时间

另一个简洁性会影响测试的领域是处理当前时间。

“简洁” 方法(直接访问)

// discount.ts
export const isDiscountExpired = (expiryDate: Date): boolean => {
  // 简洁在这里占优势:
  const now = new Date();
  return now > expiryDate;
};

问题:该函数是非确定性的。今天通过的测试明天可能会失败。要测试它,通常需要使用笨重的“假定时器”来冻结系统时钟。

“可测试” 方法(依赖注入)

通过带默认值的参数注入时间来源——一种轻量级的依赖注入形式。

// discount.ts
export const isDiscountExpired = (
  expiryDate: Date,
  now: Date = new Date() // 默认值保持应用代码简洁
): boolean => {
  return now > expiryDate;
};

现在测试变得简单且确定——无需模拟系统时钟。

// ✅ 干净的测试
test('checks expiration', () => {
  const fixedNow = new Date('2024-01-01T10:00:00Z');
  const tomorrow = new Date('2024-01-02T10:00:00Z');

  expect(isDiscountExpired(tomorrow, fixedNow)).toBe(false);
});

高级工程师的思维方式

Junior / mid‑level developers 通常以 velocity(交付速度)来衡量成功——即功能上线的快慢。简洁的代码可以提升短期速度。

Senior / principal engineers 则将关注点转向 maintainability, stability, and risk reduction(可维护性、稳定性和风险降低)。

Shift‑Left

我们希望在 bug 上实现 “shift left”:在开发者本机的单元测试中尽早捕获,而不是等到 QA 或生产环境后期才发现。
如果代码简短却依赖全局状态(import.meta.envnew Date() 等),开发者往往会本能地回避编写测试,因为搭建测试环境很痛苦。通过引入 seam(获取函数、可注入的依赖)可以让测试变得容易,从而鼓励更健康的测试文化。

通过引入少量样板代码、创建获取函数、注入依赖以及构建 seam,我们降低了编写测试的阻力。

结论

选择简洁 用于一次性原型、简单脚本或极其受限且没有逻辑的 UI 组件。

选择可测试性 用于业务逻辑、配置、辅助工具,以及任何你的应用随时间需要正确运行的依赖。

看起来今天代码会更多,但它为你换来明日的安心。

如果对你有帮助,请点个赞!❤️

#Hash

Back to Blog

相关文章

阅读更多 »