testdouble.js

testdouble.js

JavaScript 测试替身库助力测试驱动开发

testdouble.js 是一款专为 JavaScript 测试设计的替身库,支持 Node.js 和浏览器环境,兼容多种测试框架。它提供替换依赖、创建测试替身和配置存根响应等核心功能,致力于帮助开发者编写简洁、清晰的隔离测试。这个库特别适合实践测试驱动开发的团队,能有效提升测试效率和代码质量。

testdouble.js测试替身模拟库单元测试JavaScript测试Github开源项目

testdouble.js(又称 td.js)

npmjs unpkg

欢迎!您是否正在编写JavaScript测试,并且正在寻找一个模拟库来为您伪造真实对象?testdouble.js是一个有主见的、精心设计的测试替身库,由一个同样名为Test Double的软件代理公司维护。("测试替身"这个术语是由Gerard Meszaros在他的书xUnit测试模式中首次提出的。)

如果您实践测试驱动开发,testdouble.js的设计旨在促进简洁、清晰且易于理解的测试。这里有很多内容需要涵盖,所以请花些时间阅读我们的文档,它旨在向您展示如何在测试中充分利用测试替身。

这个库被设计为同时适用于Node.js和浏览器解释器。它也与测试框架无关,因此您可以将它直接应用到使用Jasmine、Mocha、Tape、Jest或我们自己的teenytest的代码库中。

安装

$ npm install -D testdouble

如果你只想获取浏览器版本,也可以从unpkg使用curl下载。

我们建议在测试辅助文件中引入该库,并为方便起见将其设置为全局变量,使用简写 td

// ES 导入语法 import * as td from 'testdouble' // CommonJS 模块(如 Node.js) globalThis.td = require('testdouble') // 在浏览器版本中设置为全局变量 window.td

(你可能需要配置你的代码检查工具以忽略 td 全局变量。 操作说明: eslint, standard。)

如果你正在将 testdouble.js 与其他测试框架一起使用,你可能还想查看以下这些扩展:

入门指南

模拟库常常被滥用而非有效使用,因此如何编写模拟库的文档以仅鼓励健康使用已成为一个真正的挑战。以下是我们为您准备的几种入门 testdouble.js 的方法:

当然,如果您不确定如何使用 testdouble.js 编写隔离测试,我们欢迎您在 GitHub 上开一个 issue 来提问

API

使用 td.replace()td.replaceEsm() 替换依赖项

测试替身库首先需要提供一种方法,让你能够用测试控制的假对象替换被测试对象的生产依赖项。

我们提供了一个名为 td.replace() 的顶级函数,它有两种不同的工作模式:CommonJS 模块替换和对象属性替换。默认情况下,这两种模式都会对真实依赖项进行深度克隆,并将遇到的所有函数替换为假的测试替身函数。这些函数可以由你的测试配置,用于模拟响应或断言调用。

对于 ES 模块,你应该使用 td.replaceEsm()。更多详情请参阅这里

Node.js 的模块替换

td.replace('../path/to/module'[, customReplacement])

如果你使用 Node.js,并且不介意在测试中使用 CommonJS 的 require() 函数(你仍然可以在生产代码中使用 import/export,假设你将其编译为测试可用的形式),testdouble.js 使用我们编写的名为 quibble 的库来修改 require() 函数,使你的被测试对象自动接收到你伪造的依赖项。如果你使用过类似 proxyquire 的工具,这种方法可能会让你感到熟悉,但我们的重点是实现更简洁的测试设置。

以下是在 Node.js 测试的设置中使用 td.replace() 的示例:

let loadsPurchases, generatesInvoice, sendsInvoice, subject module.exports = { beforeEach: () => { loadsPurchases = td.replace('../src/loads-purchases') generatesInvoice = td.replace('../src/generates-invoice') sendsInvoice = td.replace('../src/sends-invoice') subject = require('../src/index') }, //… afterEach: function () { td.reset() } }

在上面的例子中,当 src/index 被 require 时,模块缓存将被绕过。如果 index 随后 require 了任何被 td.replace() 替换的依赖项,它将收到与测试中返回的相同的假依赖项的引用。

由于 td.replace() 首先加载实际文件,它会尽力返回一个与真实对象形状完全相同的假对象。这意味着如果 loads-purchases 导出一个函数,将创建并返回一个测试替身函数。如果 generates-invoice 导出一个构造函数,将返回一个构造函数测试替身,包括所有原始静态函数和实例方法的测试替身。如果 sends-invoice 导出一个包含函数属性的普通对象,将返回一个对象,其中函数属性被替换为测试替身函数。在所有情况下,任何非函数属性都将被深度克隆。

使用 td.replace() 替换 Node.js 模块时,需要注意以下几点重要事项:

  • 测试必须在 before-each 钩子中 td.replace()require() 所有内容,以绕过 Node.js 模块缓存并避免测试之间的污染
  • 传递给 td.replace() 的任何相对路径都是从测试到依赖项的相对路径。这与其他一些工具的做法相反,但我们认为这更有意义
  • 测试套件(通常在全局 after-each 钩子中)必须调用 td.reset() 以确保在每个测试用例之后恢复真实的 require() 函数和依赖模块
ES 模块的默认导出

如果你的模块使用 ES 模块语法编写,并且指定了默认导出(例如 export default function loadsPurchases()),但实际上被转译为 CommonJS,请记住在转换为 CJS 模块格式时需要引用 .default

这意味着,不要这样写:

loadsPurchases = td.replace('../src/loads-purchases')

你可能想要这样分配假对象:

loadsPurchases = td.replace('../src/loads-purchases').default

属性替换

td.replace(containingObject, nameOfPropertyToReplace[, customReplacement])

如果你在 Node.js 环境之外运行测试,或者手动注入依赖项(或使用像 dependable 这样的依赖注入工具),你仍然可以使用 td.replace 来自动替换可作为对象属性引用的内容。

为了说明这一点,假设我们的被测试对象依赖于下面的 app.signup

app.signup = { onSubmit: function () {}, onCancel: function () {} }

如果我们的目标是在测试 app.user.create() 时替换 app.signup,我们的测试设置可能如下所示:

let signup, subject module.exports = { beforeEach: function () { signup = td.replace(app, 'signup') subject = app.user }, // … afterEach: function () { td.reset() } }

td.replace() 总是会返回新创建的假仿制品,尽管在这种情况下,测试和被测试对象显然仍然可以通过 app.signup 引用它。如果出于某种原因我们只想替换 onCancel 函数(尽管在这种情况下,这会让人觉得像是部分模拟),我们可以改为调用 td.replace(app.signup, 'onCancel')

记得在 after-each 钩子中调用 td.reset()(最好是全局调用,这样就不必在每个测试中都记得这样做),以便 testdouble.js 可以恢复原始对象。这对于避免难以调试的测试污染至关重要!

指定自定义替换

库的仿制功能相当复杂,但并不完美。对于大型、复杂的对象,它也会比较慢。如果你想精确指定用什么来替换真实依赖项,你可以在上述两种模式中通过提供一个可选的最终参数来实现。

替换 Node.js 模块时:

generatesInvoice = td.replace('../generates-invoice', { generate: td.func('a generate function'), name: 'fake invoices' })

替换属性时:

signup = td.replace(app, 'signup', { onSubmit: td.func('fake submit handler'), onCancel: function () { throw Error('do not call me') } })

td.func()td.object()td.constructor()td.instance()td.imitate() 用于创建测试替身

虽然 td.replace() 的模仿和注入便利性在项目构建配置允许的情况下很棒,但在许多情况下,你可能想要或需要直接控制创建假对象。每个创建函数既可以模仿真实对象,也可以通过传递一些配置来指定。每个测试替身创建函数都非常灵活,可以接受各种输入。返回的内容通常取决于传入的配置参数的数量和类型,因此我们将分别用示例调用来突出每种支持的用法:

td.func()

td.func() 函数(也可用作 td.function())返回一个测试替身函数,可以通过三种模式调用:

  • td.func(someRealFunction) - 返回一个与原函数同名的测试替身函数,包括对其所有自定义属性的深度模仿
  • td.func() - 返回一个匿名测试替身函数,可用于存根和验证对它的任何调用,但其错误消息和调试输出不会有名称可追溯
  • td.func('some name') - 返回一个名为 'some name' 的测试替身函数,该名称将出现在任何错误消息中,以及通过将返回的测试替身传入 td.explain() 得到的调试信息中
  • td.func<Type>() - 返回一个模仿传入类型的测试替身函数。示例和更多细节可以在 使用 TypeScript 中找到

td.object()

td.object() 函数返回一个包含测试替身函数的对象,支持三种调用类型:

  • td.object(realObject) - 返回传入对象的深度模仿,其中每个函数都被替换为一个以属性路径命名的测试替身函数(例如,如果 realObject.invoices.send() 是一个函数,返回的对象会将属性 invoices.send 设置为一个名为 '.invoices.send' 的测试替身)
  • td.object(['add', 'subtract']) - 返回一个普通的 JavaScript 对象,包含两个属性 addsubtract,它们分别被赋值为名为 '.add''.subtract' 的测试替身函数
  • td.object('a Person'[, {excludeMethods: ['then']}) - 当不带参数或第一个参数为字符串名称时,返回一个 ES Proxy。该代理将自动拦截对它的任何调用,并插入一个可用于存根或验证的测试替身。更多详情可以在 我们的完整文档 中找到
  • td.object<Interface>() - 返回一个对象,其方法作为测试替身暴露,并按照传入的接口进行类型化。示例和更多细节可以在 使用 TypeScript 中找到

td.constructor()

如果你的代码依赖于 ES 类或意图通过 new 调用的函数,那么 td.constructor() 函数也可以替换这些依赖。

  • td.constructor(RealConstructor) - 返回一个构造函数,其调用可以被验证,并且其静态和 prototype 函数都已被替换为测试替身函数,使用与 td.func(realFunction)td.object(realObject) 相同的模仿机制
  • td.constructor(['select', 'save']) - 返回一个构造函数,其 prototype 对象上的 selectsave 属性被设置为名为 '#select''#save' 的测试替身函数

在替换构造函数时,测试通常会通过直接寻址其原型函数来配置存根和验证。为了说明这一点,这意味着在你的测试中你可能会这样写:

const FakeConstructor = td.constructor(RealConstructor) td.when(FakeConstructor.prototype.doStuff()).thenReturn('ok') subject(FakeConstructor)

这样在你的生产代码中你可以:

const subject = function (SomeConstructor) { const thing = new SomeConstructor() return thing.doStuff() // 返回 "ok" }

td.instance()

作为一种简便方法,td.instance() 函数将调用 td.constructor() 并返回它返回的假构造函数的 new 实例。

以下代码片段在功能上是等价的:

const fakeObject = td.instance(RealConstructor)
const FakeConstructor = td.constructor(RealConstructor) const fakeObject = new FakeConstructor()

td.imitate()

td.imitate(realThing[, name])

如果你知道你想模仿某物,但不知道(或不关心)它是函数、对象还是构造函数,你也可以直接将它传递给 td.imitate(),并带有一个可选的名称参数。

td.when() 用于存根响应

td.when(__rehearsal__[, options])

一旦你用测试替身函数替换了被测对象的依赖项,你就会想要能够存根返回值(以及其他类型的响应),以便在被测对象以测试预期的方式调用测试替身时使用。为了使存根配置易于阅读和搜索,td.when() 的第一个参数实际上不是一个参数,而是一个占位符,用于演示你期望被测对象如何调用测试替身,如下所示:

const increment = td.func() td.when(increment(5)).thenReturn(6)

我们称 increment(5) 为"演练调用"。请注意,默认情况下,只有当被测对象完全按照演练的方式调用测试替身时,存根才会被满足。这可以通过参数匹配器进行自定义,允许进行像 increment(td.matchers.isA(Number))save(td.matchers.contains({age: 21})) 这样的演练。

还要注意,td.when() 接受一个可选的配置对象作为第二个参数,这能够实现高级用法,如忽略多余的参数和限制存根可以被满足的次数。

调用 td.when() 会返回一些函数,允许你指定当测试替身按照你的演练被调用时想要的结果。我们将从最常见的 thenReturn 开始。

td.when().thenReturn()

td.when(__rehearsal__[, options]).thenReturn('某个值'[, 更多, 值])

最简单的例子是当你想为已知参数返回一个特定值时,如下所示:

const loadsPurchases = td.replace('../src/loads-purchases') td.when(loadsPurchases(2018, 8)).thenReturn(['一次购买', '另一次'])

然后,在被测对象的使用中:

loadsPurchases(2018, 8) // 返回 `['一次购买', '另一次']` loadsPurchases(2018, 7) // 返回 undefined,因为没有满足的存根

如果你不习惯存根,可能会觉得测试知道要传入和期望从依赖项返回的确切参数有些刻意,但在孤立的单元测试中,这不仅是可行的,而且是完全正常和预期的!这样做有助于确保测试保持最小化,并对未来的读者来说显而易见。

还要注意,可以通过向 thenReturn() 传递额外的参数来存根后续匹配的调用,如下所示:

const hitCounter = td.func() td.when(hitCounter()).thenReturn(1, 2, 3, 4) hitCounter() // 1 hitCounter() // 2 hitCounter() // 3 hitCounter() // 4 hitCounter() // 4

td.when().thenResolve()td.when().thenReject()

td.when(__rehearsal__[, options]).thenResolve('某个值'[, 更多, 值]) td.when(__rehearsal__[, options]).thenReject('某个值'[, 更多, 值])

thenResolve()thenReject() 存根会将传递给它们的任何值包装在一个立即解决或拒绝的 Promise 中。默认情况下,testdouble.js 将使用全局定义的 Promise,但你可以指定自己的 Promise,如下所示:

td.config({promiseConstructor: require('bluebird')})

因为 Promise 规范指出所有 Promise 必须触发事件循环,请记住,任何用 thenResolvethenReject 配置的存根都必须作为异步测试进行管理(如果你不确定,请查阅你的测试框架文档)。

td.when().thenCallback()

td.when(__rehearsal__[, options]).thenCallback('某个值'[,其他,参数])

thenCallback() 存根会假设演练的调用有一个额外的最后参数,该参数接受一个回调函数。当这个存根被满足时,testdouble.js 将调用该回调函数并传入传递给 thenCallback() 的任何参数。

为了说明,考虑这个存根:

const readFile = td.replace('../src/read-file') td.when(readFile('my-secret-doc.txt')).thenCallback(null, '秘密!')

然后,被测对象可能会调用 readFile 并传入一个匿名函数:

readFile('my-secret-doc.txt', function (err, contents) { console.log(contents) // 将打印 '秘密!' })

如果回调不在最后位置,或者测试替身也需要返回某些内容,可以使用 td.callback 参数匹配器来配置回调。

一方面,thenCallback() 可以是编写快速清晰的同步隔离单元测试的好方法,即使实际生产代码是异步的。另一方面,如果需要验证被测对象在多个事件循环周期内的正确行为,可以使用 deferdelay 选项 来控制这一点。

td.when().thenThrow()

td.when(__rehearsal__[, options]).thenThrow(new Error('炸了'))

thenThrow() 函数完全按照其名称所示工作。一旦配置了这个存根,任何匹配的调用都会抛出指定的错误。

请注意,由于演练调用会调用测试替身函数,因此可能会在配置 thenThrow 存根时意外触发它,然后在尝试配置后续存根或验证时再次触发。在这些情况下,你需要通过重新排序配置或捕获错误来解决这个问题。

td.when().thenDo()

td.when(__rehearsal__[, options]).thenDo(function (arg1, arg2) {})

对于其他所有情况,我们有 thenDo()thenDo 接受一个函数,该函数将在存根被满足时调用,并传入所有参数,且绑定到测试替身函数实际被调用时的相同 this 上下文。你的 thenDo 函数返回的任何内容都将在存根被满足时由测试替身返回。这种配置对于处理其他方法无法处理的棘手情况很有用,可能是构建库存根功能的潜在扩展点。

td.verify() 用于验证交互

td.verify(__demonstration__[, options])

如果你已经学会了如何使用 td.when() 来存根响应,那么你就已经知道如何使用 td.verify() 来验证调用是否发生了!我们特意让这两者尽可能保持对称。你会发现它们有相同的函数签名,支持相同的参数匹配器,并接受相同的选项。

那么,它们的区别在于用途。存根是为了促进我们想要在主体中执行的某些行为,而验证是为了确保依赖项以特定的预期方式被调用。由于 td.verify() 是一个断言步骤,它应该放在测试的末尾,在我们调用被测主体之后。

一个简单的例子可能是:

module.exports = function shouldSaveThings () { const save = td.replace('../src/save') const subject = require('../src/index') subject({name: 'dataz', data: '010101'}) td.verify(save('dataz', '010101')) }

上面的代码将验证 save 是否使用了指定的两个参数进行调用。如果验证失败(比如传递了 '010100' 而不是 '010101'),testdouble.js 将抛出一个详细的错误消息,解释测试替身函数实际上是如何被调用的,希望能帮助你发现错误。

就像 td.when() 一样,更复杂的情况可以通过参数匹配器和配置选项来处理。

需要注意的是:td.verify() 应该只在少数情况下使用。当你验证一个函数被调用(而不是依赖它的返回值)时,你是在断言你的主体有一个副作用。有大量副作用的代码是不好的,因此模拟库常常被滥用来使具有大量副作用的代码更容易扩散。在这些情况下,将每个依赖项重构为返回值几乎总是更好的设计方法。验证调用的另一个测试异味是,有时为了最大程度的完整性,测试会验证一个已经满足存根的调用,但这几乎是可以证明是不必要的。

td.listReplacedModules() 用于列出被替换的模块

td.listReplacedModules()

使用 td.listReplacedModules() 来列出被替换的模块。此函数将返回一个数组,包含当前通过 td.replace()td.replaceEsm() 被替换的模块。

列表没有特定顺序,返回的是被替换模块的完整路径。

路径以 file: URL 的形式返回,这是 ESM 中的惯例(即使被替换的模块是 CJS,也是如此)。

例如,如果你执行以下操作:

td.replace('../src/save')

那么

td.listReplacedModules()

将返回类似这样的结果:

['file:///users/example/code/foo/src/save.js']

其他功能

关于testdouble.js API中其他顶级功能,请参阅docs目录:

编辑推荐精选

扣子-AI办公

扣子-AI办公

职场AI,就用扣子

AI办公助手,复杂任务高效处理。办公效率低?扣子空间AI助手支持播客生成、PPT制作、网页开发及报告写作,覆盖科研、商业、舆情等领域的专家Agent 7x24小时响应,生活工作无缝切换,提升50%效率!

堆友

堆友

多风格AI绘画神器

堆友平台由阿里巴巴设计团队创建,作为一款AI驱动的设计工具,专为设计师提供一站式增长服务。功能覆盖海量3D素材、AI绘画、实时渲染以及专业抠图,显著提升设计品质和效率。平台不仅提供工具,还是一个促进创意交流和个人发展的空间,界面友好,适合所有级别的设计师和创意工作者。

图像生成AI工具AI反应堆AI工具箱AI绘画GOAI艺术字堆友相机AI图像热门
码上飞

码上飞

零代码AI应用开发平台

零代码AI应用开发平台,用户只需一句话简单描述需求,AI能自动生成小程序、APP或H5网页应用,无需编写代码。

Vora

Vora

免费创建高清无水印Sora视频

Vora是一个免费创建高清无水印Sora视频的AI工具

Refly.AI

Refly.AI

最适合小白的AI自动化工作流平台

无需编码,轻松生成可复用、可变现的AI自动化工作流

酷表ChatExcel

酷表ChatExcel

大模型驱动的Excel数据处理工具

基于大模型交互的表格处理系统,允许用户通过对话方式完成数据整理和可视化分析。系统采用机器学习算法解析用户指令,自动执行排序、公式计算和数据透视等操作,支持多种文件格式导入导出。数据处理响应速度保持在0.8秒以内,支持超过100万行数据的即时分析。

AI工具酷表ChatExcelAI智能客服AI营销产品使用教程
TRAE编程

TRAE编程

AI辅助编程,代码自动修复

Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。

AI工具TraeAI IDE协作生产力转型热门
AIWritePaper论文写作

AIWritePaper论文写作

AI论文写作指导平台

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

AI辅助写作AI工具AI论文工具论文写作智能生成大纲数据安全AI助手热门
博思AIPPT

博思AIPPT

AI一键生成PPT,就用博思AIPPT!

博思AIPPT,新一代的AI生成PPT平台,支持智能生成PPT、AI美化PPT、文本&链接生成PPT、导入Word/PDF/Markdown文档生成PPT等,内置海量精美PPT模板,涵盖商务、教育、科技等不同风格,同时针对每个页面提供多种版式,一键自适应切换,完美适配各种办公场景。

AI办公办公工具AI工具博思AIPPTAI生成PPT智能排版海量精品模板AI创作热门
潮际好麦

潮际好麦

AI赋能电商视觉革命,一站式智能商拍平台

潮际好麦深耕服装行业,是国内AI试衣效果最好的软件。使用先进AIGC能力为电商卖家批量提供优质的、低成本的商拍图。合作品牌有Shein、Lazada、安踏、百丽等65个国内外头部品牌,以及国内10万+淘宝、天猫、京东等主流平台的品牌商家,为卖家节省将近85%的出图成本,提升约3倍出图效率,让品牌能够快速上架。

下拉加载更多