testdouble.js 依赖替换技术详解
什么是依赖替换
在单元测试中,我们经常需要将被测对象与它的依赖项隔离开来。testdouble.js 提供了强大的依赖替换功能,可以让我们用测试替身(Test Double)替换真实的依赖项。这种技术对于编写隔离测试和进行测试驱动开发(TDD)非常有价值。
两种替换机制
testdouble.js 提供了两种主要的依赖替换方式:
1. 对象属性替换
通过 td.replace(someObject, 'propertyName')
可以替换对象的某个属性。这种方式会:
- 保留原始属性的引用
- 在测试期间用测试替身替换该属性
- 在调用
td.reset()
后恢复原始属性
2. 模块替换
通过 td.replace('../path/to/module')
可以替换 Node.js 模块。这种方式会:
- 拦截对该模块的
require
调用 - 返回测试替身而非真实模块
- 同样在
td.reset()
后恢复原始模块
自动模仿机制
testdouble.js 会根据原始依赖的类型自动创建合适的测试替身:
- 普通函数:替换为同名的测试替身函数,所有属性会被深拷贝,其中的函数属性也会被替换
- 普通对象:深拷贝对象,所有方法替换为测试替身
- 构造函数/ES类:创建人工构造函数,所有静态和原型方法替换为测试替身
ES 模块支持
对于 ES 模块(使用 import/export
语法),需要使用 td.replaceEsm
方法:
// 示例:替换 ES 模块
const brakeModule = await td.replaceEsm('../../lib/brake.mjs')
与 td.replace
不同,td.replaceEsm
是异步的,需要配合 await
使用。它返回一个包含模块导出内容的对象,如果有默认导出,会包含在 default
属性中。
使用注意事项
-
生命周期管理:
- 将替换和导入操作放在
beforeEach
钩子中 - 确保在
afterEach
中调用td.reset()
- 将替换和导入操作放在
-
模块加载顺序:
- 对于 CommonJS,必须在
require
前调用td.replace
- 对于 ES 模块,可以在导入后调用
td.replaceEsm
- 对于 CommonJS,必须在
-
副作用处理:
- 被替换模块的副作用会在替换时执行
- 可以通过手动提供替身来避免(如
td.replace('./path', td.func())
)
-
TDD 工作流:
- 当模块不存在时,
td.replace
会抛出错误,指导你先创建模块
- 当模块不存在时,
实际应用示例
汽车刹车系统测试
// 测试文件
module.exports = {
beforeEach: function() {
this.brake = td.replace('../../lib/brake')
this.subject = require('../../lib/car')
},
'减速时应调用刹车': function() {
this.subject.slowDown()
td.verify(this.brake(10))
}
}
实现文件
// lib/car.js
const brake = require('./brake')
module.exports.slowDown = function() {
brake(10)
}
第三方模块替换
虽然可以替换第三方模块,但不推荐这样做,除非你拥有该模块。遵循"不要模拟你不拥有的代码"原则,保持第三方依赖的边界清晰。
浏览器环境支持
在浏览器环境中,主要使用对象属性替换方案。例如:
// 替换全局命名空间中的属性
const brake = td.replace(app, 'brake')
const subject = app.car
subject.slowDown()
td.verify(brake(10))
API 详解
td.replace
API
-
对象属性模式:
td.replace(object, propertyName, [manualReplacement])
- 第一个参数为对象
- 第二个参数为属性名(字符串)
- 可选第三个参数手动指定替换值
-
模块替换模式:
td.replace(relativePathToModule, [manualReplacement])
- 第一个参数为模块相对路径(字符串)
- 可选第二个参数手动指定替换值
td.replaceEsm
API
// 基本用法
await td.replaceEsm(relativePathToModule)
// 带替换实现的用法
await td.replaceEsm(relativePathToModule, namedExportReplacements, defaultExportReplacement)
总结
testdouble.js 的依赖替换功能为单元测试提供了强大的隔离能力。无论是对象属性还是模块依赖,都能通过简洁的 API 进行替换。正确使用这些功能可以:
- 提高测试的隔离性
- 支持更好的 TDD 工作流
- 保持测试的简洁和可维护性
记住关键原则:总是在 beforeEach
中设置替换,在 afterEach
中调用 td.reset()
,这样就能避免测试间的状态污染。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考