Node.js 测试与 mock:单元测试与模块边界
在 Node 里写测试,表面上是调 Jest / Vitest / node:test,真正费劲的往往是:异步怎么断言、模块怎么替换、I/O 要不要打桩。
这一篇不对比框架细节,而是想讲清楚几件事:测什么算「单元」、mock 在破坏什么边界、以及和 ESM/CJS 混用相关的常见坑。
测试金字塔在 Node 里的一句话版
可以粗分为三层(名字各家略有出入):
- 单元测试:单个模块、可控输入,外部依赖尽量假换或桩掉;
- 集成测试:几个真实模块 + 真实或容器化的依赖(如本机 Redis、Testcontainers);
- 端到端:整服务、真实 HTTP、黑盒行为。
Node 服务端最常见的浪费是:
- 用集成测试冒充单元测试 → 慢、飘、难定位;
- 单元测试里到处连真实网络 → 不稳定。
先想清楚「这条用例要证明什么」,再选层级。
断言异步:Promise、async/await 与计时
现代测试框架都支持 async 测试函数,核心是:
- 每个异步分支都要
await或 return Promise,否则用例可能 先绿后红(断言没跑到就结束); - fake timer(
setTimeout等)要显式切模式,否则和真实时间混在一起会 flaky。
也就是说,异步测试的可靠性,一半在框架,一半在 你是否把异步链路写完整。
mock:替换的是「边界」,不是「偷懒」
mock / stub 常见用途:
- 固定时间、随机数、UUID,让输出可断言;
- 替换网络、文件、环境变量,避免副作用;
- 验证某依赖是否被以何种参数调用(间谍 spy)。
代价是:
- mock 过多的测试会绑死在实现细节上,重构一行就全红;
- 掩盖真实集成问题——模块之间「协议」错了,单测仍全绿。
更稳的习惯:
- 对外部系统:用 接口层(repository、client)集中 mock;
- 对复杂纯函数:尽量 输入输出断言,少断言「调了几次内部函数」。
模块替换与 ESM:比 CJS 更「硬」
在 CJS 时代,不少测试工具用 重写 require 缓存 注入 mock。
ESM 下:
- 静态 import 更不利于运行时替换;
- 需要
import()、依赖注入、或 测试专用入口 等模式配合。
工程含义:
- 可测性 increasingly 等于 可注入依赖;
- 新代码里把 IO 与纯逻辑 分开,比事后补 mock 更省力。
测试数据与夹具:fixture、工厂与快照
常见手段:
- fixture 文件:JSON / SQL seed,版本和内容一起进仓库;
- 工厂函数:生成合法对象,避免每个用例手写一大坨字面量;
- 快照测试:适合 稳定输出(序列化、模板),不适合 频繁变的业务文案。
快照滥用会:
- 让「更新快照」变成无脑点确定,失去回归保护。
小结:好测试保护重构,而不是锁死实现
- 层级选对:单元快、集成准、E2E 少而关键;
- 异步与计时 要显式处理,避免假绿;
- mock 打在边界上,配合 依赖注入 与 ESM 约束 一起设计。
把测试策略和模块边界一起看,比纠结「用 Jest 还是 Vitest」更有意义——工具换一版,思路还在。