在单元测试真实世界代码时,许多情况使测试难以编写。如何检查是否调用了函数?如何测试 Ajax 调用?或者使用 setTimeout
的代码?这时,您需要使用 测试替身 —— 替换代码,使难以测试的内容易于测试。
多年来,Sinon.js 一直是 JavaScript 测试中创建测试替身的实际标准。对于任何编写测试的 JavaScript 开发人员来说,它都是必不可少的工具,因为没有它,为真实应用程序编写测试几乎是不可能的。
最近,一个名为 testdouble.js 的新库正在兴起。它拥有与 Sinon.js 类似的功能集,只是这里和那里有一些不同。
在本文中,我们将探讨 Sinon.js 和 testdouble.js 提供的内容,并比较它们各自的优缺点。Sinon.js 是否仍然是更好的选择,或者挑战者能否胜出?
注意:如果您不熟悉测试替身,建议您先阅读我的 Sinon.js 教程。这将帮助您更好地理解我们将在此处讨论的概念。
为了确保易于理解正在讨论的内容,以下是所用术语的快速概述。这些是 Sinon.js 的定义,在其他地方可能略有不同。
需要注意的是,testdouble.js 的目标之一是减少这种术语之间的混淆。
让我们首先看看 Sinon.js 和 testdouble.js 在基本用法上的比较。
Sinon 有三种不同的测试替身概念:间谍、存根和模拟。其思想是,每种都代表不同的使用场景。这使得该库对于来自其他语言或阅读过使用相同术语的书籍(例如 xUnit 测试模式)的人来说更加熟悉。但另一方面,这三种概念也可能使 Sinon 在首次使用时 更难 理解。
这是一个 Sinon 用法的基本示例:
// 以下是查看函数调用的参数的方法: var spy = sinon.spy(Math, 'abs'); Math.abs(-10); console.log(spy.firstCall.args); // 输出:[ -10 ] spy.restore(); // 以下是控制函数执行方式的方法: var stub = sinon.stub(document, 'createElement'); stub.returns('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' stub.restore();
相反,testdouble.js 选择了一个更简单的 API。它不使用间谍或存根之类的概念,而是使用 JavaScript 开发人员更熟悉的语言,例如 td.function
、td.object
和 td.replace
。这使得 testdouble 潜在地更容易上手,并且更适合某些任务。但另一方面,某些更高级的用途可能根本不可能实现(这有时是故意的)。
以下是 testdouble.js 的使用方法:
// 以下是查看函数调用的参数的方法: var abs = td.replace(Math, 'abs'); Math.abs(-10); var explanation = td.explain(abs); console.log(explanation.calls[0].args); // 输出:[ -10 ] // 以下是控制函数执行方式的方法: var createElement = td.replace(document, 'createElement'); td.when(createElement(td.matchers.anything())).thenReturn('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' // testdouble 使用一次调用重置所有测试替身,无需单独清理 td.reset();
testdouble 使用的语言更简单。我们“替换”函数而不是“存根”它。我们要求 testdouble “解释”函数以从中获取信息。除此之外,到目前为止,它与 Sinon 相当相似。
这也扩展到创建“匿名”测试替身:
var x = sinon.stub();
与
var x = td.function();
Sinon 的间谍和存根具有提供有关它们的更多信息的属性。例如,Sinon 提供了 stub.callCount
和 stub.args
等属性。在 testdouble 的情况下,我们从 td.explain
获取此信息:
// 我们也可以为测试替身命名 var x = td.function('hello'); x('foo', 'bar'); td.explain(x); console.log(x); /* 输出: { name: 'hello', callCount: 1, calls: [ { args: ['foo', 'bar'], context: undefined } ], description: 'This test double `hello` has 0 stubbings and 1 invocations.\n\nInvocations:\n - called with `("foo", "bar")`.', isTestDouble: true } */
较大的区别之一与设置存根和验证的方式有关。使用 Sinon,您可以在存根之后链接命令,并使用断言来验证结果。testdouble.js 只需向您展示您希望如何调用函数——或者如何“排练”函数调用。
var x = sinon.stub(); x.withArgs('hello', 'world').returns(true); var y = sinon.stub(); sinon.assert.calledWith(y, 'foo', 'bar');
与
var x = td.function(); td.when(x('hello', 'world')).thenReturn(true); var y = td.function(); td.verify(y('foo', 'bar'));
这使得 testdouble 的 API 更易于理解,因为您不需要知道可以在何时链接哪些操作。
在高层次上,这两个库都相当相似。但是您可能需要在实际项目中执行的常见测试任务呢?让我们看看一些差异开始显现的情况。
首先要注意的是,testdouble.js 没有“间谍”的概念。虽然 Sinon.js 允许我们替换函数调用以便从中获取信息,同时保留函数的默认行为,但这在 testdouble.js 中根本不可能。当您使用 testdouble 替换函数时,它总是会丢失其默认行为。
但这不一定是问题。间谍最常见的用法是使用它们来验证是否调用了回调,这很容易使用 td.function
来完成:
var spy = sinon.spy(); myAsyncFunction(spy); sinon.assert.calledOnce(spy);
与
var spy = td.function(); myAsyncFunction(spy); td.verify(spy());
虽然不是什么大问题,但仍然需要注意这两个库之间的这种差异,否则如果您期望能够以某种更具体的方式使用 testdouble.js 中的间谍,您可能会感到惊讶。
您会遇到的第二个区别是 testdouble 对输入更严格。
Sinon 的存根和断言都允许您对提供的参数不精确。这最容易通过示例来说明:
var stub = sinon.stub(); stub.withArgs('hello').returns('foo'); console.log(stub('hello', 'world')); // 输出:'foo' sinon.assert.calledWith(stub, 'hello'); // 没有错误
与
// 以下是查看函数调用的参数的方法: var spy = sinon.spy(Math, 'abs'); Math.abs(-10); console.log(spy.firstCall.args); // 输出:[ -10 ] spy.restore(); // 以下是控制函数执行方式的方法: var stub = sinon.stub(document, 'createElement'); stub.returns('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' stub.restore();
默认情况下,Sinon 不关心向函数提供了多少额外参数。虽然它提供了诸如 sinon.assert.calledWithExactly
之类的函数,但在文档中并不建议将其作为默认值。像 stub.withArgs
这样的函数也没有“exactly”变体。
另一方面,testdouble.js 默认情况下要求指定精确的参数。这是设计使然。其思想是,如果向函数提供了一些在测试中未指定的其他参数,则这可能是一个错误,并且应该使测试失败。
可以在 testdouble.js 中允许指定任意参数,但这并不是默认值:
// 以下是查看函数调用的参数的方法: var abs = td.replace(Math, 'abs'); Math.abs(-10); var explanation = td.explain(abs); console.log(explanation.calls[0].args); // 输出:[ -10 ] // 以下是控制函数执行方式的方法: var createElement = td.replace(document, 'createElement'); td.when(createElement(td.matchers.anything())).thenReturn('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' // testdouble 使用一次调用重置所有测试替身,无需单独清理 td.reset();
使用 ignoreExtraArgs: true
,行为类似于 Sinon.js。
虽然使用 Sinon.js 的 Promise 并不复杂,但 testdouble.js 具有返回和拒绝 Promise 的内置方法。
var x = sinon.stub();
与
var x = td.function();
注意:可以使用 sinon-as-promised 在 Sinon 1.x 中包含类似的便捷函数。Sinon 2.0 和更新版本以 stub.resolves
和 stub.rejects
的形式包含 Promise 支持。
Sinon 和 testdouble 都提供了一种简单的方法来让存根函数调用回调。但是,它们在工作方式上有一些差异。
Sinon 使用 stub.yields
来让存根调用它作为参数接收的 第一个函数。
// 我们也可以为测试替身命名 var x = td.function('hello'); x('foo', 'bar'); td.explain(x); console.log(x); /* 输出: { name: 'hello', callCount: 1, calls: [ { args: ['foo', 'bar'], context: undefined } ], description: 'This test double `hello` has 0 stubbings and 1 invocations.\n\nInvocations:\n - called with `("foo", "bar")`.', isTestDouble: true } */
testdouble.js 默认使用 Node 样式模式,其中回调被假定为 最后一个 参数。在排练调用时,您也不必指定它:
var x = sinon.stub(); x.withArgs('hello', 'world').returns(true); var y = sinon.stub(); sinon.assert.calledWith(y, 'foo', 'bar');
使 testdouble 的回调支持更强大的原因是您可以轻松定义具有多个回调或回调顺序不同的场景的行为。
假设我们想调用 callback1
……
var x = td.function(); td.when(x('hello', 'world')).thenReturn(true); var y = td.function(); td.verify(y('foo', 'bar'));
请注意,我们将 td.callback
作为参数传递给 td.when
中的函数。这告诉 testdouble 我们希望使用哪个参数作为回调。
使用 Sinon,也可以更改行为:
var spy = sinon.spy(); myAsyncFunction(spy); sinon.assert.calledOnce(spy);
在这种情况下,我们使用 callsArgWith
而不是 yields
。我们必须提供调用的特定索引才能使其工作,这可能有点麻烦,尤其是在具有许多参数的函数上。
如果我们想使用某些值调用 两个 回调呢?
var spy = td.function(); myAsyncFunction(spy); td.verify(spy());
使用 Sinon,这根本不可能。您可以链接对 callsArgWith
的多次调用,但它只会调用其中一个。
除了能够使用 td.replace
替换函数之外,testdouble 还允许您替换整个模块。
这主要在您有一个直接导出需要替换的函数的模块的情况下有用:
var stub = sinon.stub(); stub.withArgs('hello').returns('foo'); console.log(stub('hello', 'world')); // 输出:'foo' sinon.assert.calledWith(stub, 'hello'); // 没有错误
如果我们想用 testdouble 替换它,我们可以使用 td.replace('path/to/file')
,例如……
// 以下是查看函数调用的参数的方法: var spy = sinon.spy(Math, 'abs'); Math.abs(-10); console.log(spy.firstCall.args); // 输出:[ -10 ] spy.restore(); // 以下是控制函数执行方式的方法: var stub = sinon.stub(document, 'createElement'); stub.returns('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' stub.restore();
虽然 Sinon.js 可以替换某个对象的成员函数,但它不能像这样替换模块。在使用 Sinon 时要执行此操作,您需要使用另一个模块,例如 proxyquire 或 rewire。
// 以下是查看函数调用的参数的方法: var abs = td.replace(Math, 'abs'); Math.abs(-10); var explanation = td.explain(abs); console.log(explanation.calls[0].args); // 输出:[ -10 ] // 以下是控制函数执行方式的方法: var createElement = td.replace(document, 'createElement'); td.when(createElement(td.matchers.anything())).thenReturn('not an html element'); var x = document.createElement('div'); console.log(x); // 输出:'not an html element' // testdouble 使用一次调用重置所有测试替身,无需单独清理 td.reset();
关于模块替换需要注意的另一件事是 testdouble.js 会自动替换整个模块。如果它像这里的示例一样是函数导出,它会替换该函数。如果它是一个包含多个函数的对象,它会替换所有这些函数。构造函数和 ES6 类也受支持。proxyquire 和 rewire 都要求您单独指定要替换的内容和方式。
如果您使用 Sinon 的模拟计时器、模拟 XMLHttpRequest 或模拟服务器,您会注意到它们在 testdouble 中不存在。
模拟计时器可以作为插件使用,但 XMLHttpRequests 和 Ajax 功能需要以不同的方式处理。
一个简单的解决方案是替换您正在使用的 Ajax 函数,例如 $.post
:
var x = sinon.stub();
对于 Sinon.js 的初学者来说,一个常见的绊脚石往往是清理间谍和存根。Sinon 提供了 三种 不同的方法来做到这一点,这并没有什么帮助。
var x = td.function();
或:
// 我们也可以为测试替身命名 var x = td.function('hello'); x('foo', 'bar'); td.explain(x); console.log(x); /* 输出: { name: 'hello', callCount: 1, calls: [ { args: ['foo', 'bar'], context: undefined } ], description: 'This test double `hello` has 0 stubbings and 1 invocations.\n\nInvocations:\n - called with `("foo", "bar")`.', isTestDouble: true } */
或:
var x = sinon.stub(); x.withArgs('hello', 'world').returns(true); var y = sinon.stub(); sinon.assert.calledWith(y, 'foo', 'bar');
通常情况下,建议使用 sandbox 和 sinon.test
方法,否则很容易意外地留下存根或间谍,这可能会导致其他测试出现问题。这可能会导致难以追踪的级联故障。
testdouble.js 只提供了一种清理测试替身的方法:td.reset()
。推荐的方法是在 afterEach
钩子中调用它:
var x = td.function(); td.when(x('hello', 'world')).thenReturn(true); var y = td.function(); td.verify(y('foo', 'bar'));
这极大地简化了测试替身的设置和测试后的清理,降低了难以追踪的错误的可能性。
我们现在已经了解了这两个库的功能。它们都提供了一套相当类似的功能集,但它们彼此的设计理念有所不同。我们能否将其分解为优缺点?
让我们首先谈谈 Sinon.js。它提供了一些比 testdouble.js 更多的附加功能,并且它的一些方面更易于配置。这为它在更特殊的测试场景中提供了一些更高的灵活性。Sinon.js 还使用更熟悉其他语言的人的语言——间谍、存根和模拟等概念存在于不同的库中,并且也在测试相关的书籍中讨论过。
缺点是增加了复杂性。虽然它的灵活性允许专家做更多的事情,但这意味着某些任务比在 testdouble.js 中更复杂。对于那些刚接触测试替身概念的人来说,它也可能具有更陡峭的学习曲线。事实上,即使像我这样非常熟悉它的人也可能难以详细说明 sinon.stub
和 sinon.mock
之间的某些区别!
testdouble.js 选择了一个更简单的接口。它的大部分内容都相当简单易用,并且感觉更适合 JavaScript,而 Sinon.js 有时感觉像是为其他语言设计的。由于这一点及其一些设计原则,它对于初学者来说更容易上手,即使是经验丰富的测试人员也会发现许多任务更容易完成。例如,testdouble 使用相同的 API 来设置测试替身和验证结果。由于其更简单的清理机制,它也可能更不容易出错。
testdouble 最大的问题是由它的一些设计原则造成的。例如,完全缺乏间谍可能会使某些更喜欢使用间谍而不是存根的人无法使用它。这在很大程度上是一个意见问题,您可能根本不会发现问题。除此之外,尽管 testdouble.js 是一个更新的库,但它正在为 Sinon.js 提供一些严重的竞争。
以下是按功能的比较:
功能 | Sinon.js | testdouble.js |
---|---|---|
间谍 | 是 | 否 |
存根 | 是 | 是 |
延迟存根结果 | 否 | 是 |
模拟 | 是 | 是1 |
Promise 支持 | 是(在 2.0 中) | 是 |
时间辅助函数 | 是 | 是(通过插件) |
Ajax 辅助函数 | 是 | 否(改为替换函数) |
模块替换 | 否 | 是 |
内置断言 | 是 | 是 |
匹配器 | 是 | 是 |
自定义匹配器 | 是 | 是 |
参数捕获器 | 否2 | 是 |
代理测试替身 | 否 | 是 |
td.replace(someObject)
来实现类似的效果。stub.yield
(不要与 stub.yields
混淆)来实现与参数捕获器类似的效果。Sinon.js 和 testdouble.js 都提供了一套相当类似的功能。在这方面,两者都不明显优越。
两者之间最大的区别在于它们的 API。Sinon.js 可能稍微冗长一些,同时提供了许多关于如何做事情的选项。这可能是它的优点和缺点。testdouble.js 具有更精简的 API,这使其更容易学习和使用,但由于其更武断的设计,有些人可能会发现它有问题。
您是否同意 testdouble 的设计原则?如果是,那么没有理由不使用它。我在许多项目中都使用过 Sinon.js,我可以肯定地说 testdouble.js 至少完成了我在 Sinon.js 中完成的 95% 的工作,其余 5% 可能可以通过一些简单的解决方法来完成。
如果您发现 Sinon.js 难以使用,或者正在寻找更“JavaScript 式”的测试替身方法,那么 testdouble.js 也可能适合您。即使像我这样花了很多时间学习使用 Sinon 的人,我也倾向于建议尝试 testdouble.js 并看看您是否喜欢它。
但是,testdouble.js 的某些方面可能会让那些了解 Sinon.js 或其他经验丰富的测试人员头疼。例如,完全缺乏间谍可能是决定性因素。对于专家和那些想要最大灵活性的用户来说,Sinon.js 仍然是一个不错的选择。
如果您想了解更多关于如何在实践中使用测试替身的信息,请查看我的免费 Sinon.js 实战指南。虽然它使用 Sinon.js,但您也可以将相同的技术和最佳实践应用于 testdouble.js。
有问题?评论?您是否已经在使用 testdouble.js 了?阅读本文后,您是否会考虑尝试一下?请在下面的评论中告诉我。
本文由 James Wright、Joan Yin、Christian Johansen 和 Justin Searls 共同评审。感谢所有 SitePoint 的同行评审者,使 SitePoint 内容达到最佳状态!
Sinon.js 和 Testdouble.js 都是流行的 JavaScript 测试库,但它们有一些关键区别。Sinon.js 以其丰富的功能集而闻名,包括间谍、存根和模拟,以及用于模拟计时器和 XHR 的实用程序。它是一个多功能工具,可以与任何测试框架结合使用。另一方面,Testdouble.js 是一个极简的库,专注于为测试替身提供简单直观的 API,测试替身是待测系统部分的替代品。它不包括用于模拟计时器或 XHR 的实用程序,但它设计易于使用和理解,因此对于那些更喜欢更精简的测试方法的人来说是一个不错的选择。
Sinon.js 和 Testdouble.js 都可以通过 npm(Node.js 包管理器)安装。对于 Sinon.js,您可以使用命令 npm install sinon
。对于 Testdouble.js,命令是 npm install testdouble
。安装后,您可以分别使用 const sinon = require('sinon')
和 const td = require('testdouble')
在您的测试文件中引入它们。
是的,可以在同一个项目中同时使用 Sinon.js 和 Testdouble.js。它们都设计得非常简洁,并且可以很好地与其他库一起工作。但是,请记住它们具有重叠的功能,因此同时使用它们可能会导致混淆。通常建议根据您的具体需求和偏好选择其中一个。
在 Sinon.js 中,您可以使用 sinon.spy()
创建间谍。此函数返回一个间谍对象,该对象记录对其进行的所有调用,包括参数、返回值和异常。在 Testdouble.js 中,您可以使用 td.function()
创建间谍。此函数返回一个记录所有调用的测试替身函数,包括参数。
在 Sinon.js 中,您可以使用 sinon.stub()
创建存根。此函数返回一个存根对象,其行为类似于间谍,但它还允许您定义其行为,例如指定返回值或抛出异常。在 Testdouble.js 中,您可以使用 td.when()
创建存根。此函数允许您在使用特定参数调用测试替身时定义其行为。
在 Sinon.js 中,您可以使用 spy.called
、spy.calledWith()
和 spy.returned()
等方法来验证间谍或存根。在 Testdouble.js 中,您可以使用 td.verify()
来断言测试替身是否以某种方式被调用。
与 Testdouble.js 相比,Sinon.js 具有更全面的功能集。它包括用于模拟计时器和 XHR 的实用程序,这对于测试某些类型的代码非常有用。它也更广泛地使用,并且拥有更大的社区,这意味着可以获得更多资源和支持。
与 Sinon.js 相比,Testdouble.js 具有更简单直观的 API。它设计易于使用和理解,因此对于那些更喜欢更精简的测试方法的人来说是一个不错的选择。它还通过使滥用测试替身变得困难来鼓励良好的测试实践。
是的,Sinon.js 和 Testdouble.js 都设计得非常简洁,并且可以很好地与其他测试框架一起工作。它们可以与任何支持 JavaScript 的测试框架一起使用。
是的,Sinon.js 和 Testdouble.js 在其官方网站上都有大量的文档。还有许多教程、博客文章和在线课程涵盖了这些库的深入内容。
以上是JavaScript测试工具摊牌:sinon.js vs testdouble.js的详细内容。更多信息请关注PHP中文网其他相关文章!