权衡:干净的测试 vs. 代码简洁性(现代 JS)
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会在测试文件加载时 立即 求值。 - 一旦常量被设为
true或false,在同一次测试运行中几乎不可能再更改它。
为了同时测试两种情形,你常常需要“全局存根”,例如让 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.env、new Date() 等),开发者往往会本能地回避编写测试,因为搭建测试环境很痛苦。通过引入 seam(获取函数、可注入的依赖)可以让测试变得容易,从而鼓励更健康的测试文化。
通过引入少量样板代码、创建获取函数、注入依赖以及构建 seam,我们降低了编写测试的阻力。
结论
选择简洁 用于一次性原型、简单脚本或极其受限且没有逻辑的 UI 组件。
选择可测试性 用于业务逻辑、配置、辅助工具,以及任何你的应用随时间需要正确运行的依赖。
看起来今天代码会更多,但它为你换来明日的安心。
如果对你有帮助,请点个赞!❤️
#Hash