Home >Web Front-end >JS Tutorial >JavaScript Testing Tool Showdown: Sinon.js vs testdouble.js
When unit testing real-world code, many situations make the test difficult to write. How to check if a function is called? How to test Ajax calls? Or use the code of setTimeout
? At this time, you need to use to test the stand-in - replace the code to make it easier to test within the hard-to-test.
For many years, Sinon.js has been the actual standard for creating test stand-ins in JavaScript testing. It is an essential tool for any JavaScript developer who writes tests, because without it, it is nearly impossible to write tests for real applications.
Recently, a new library called testdouble.js is emerging. It has a similar feature set to Sinon.js, except that there are a few differences here and there.
In this article, we will explore what Sinon.js and testdouble.js provide and compare their respective pros and cons. Is Sinon.js still a better choice, or can the challenger win?
Note: If you are not familiar with testing stand-ins, it is recommended that you read my Sinon.js tutorial first. This will help you better understand the concepts we will discuss here.
To ensure easy understanding of what is being discussed, here is a quick overview of the terms used. These are the definitions of Sinon.js and may vary slightly elsewhere.
It should be noted that one of the goals of testdouble.js is to reduce confusion between this term.
Let's first look at the basic usage comparison between Sinon.js and testdouble.js.
Sinon has three different test stand-in concepts: spy, stubs, and mocks. The idea is that each represents a different usage scenario. This makes the library more familiar to people from other languages or who have read books using the same terminology, such as xUnit testing mode. But on the other hand, these three concepts may also make Sinon more difficult to understand when first used. This is a basic example of Sinon usage:
<code class="language-javascript">// 以下是查看函数调用的参数的方法: 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();</code>
In contrast, testdouble.js chooses a simpler API. Instead of using concepts like spies or stubs, it uses languages that JavaScript developers are more familiar with, such as td.function
, td.object
and td.replace
. This makes testdouble potentially easier to get started and more suitable for certain tasks. But on the other hand, some more advanced uses may simply not be possible (which is sometimes intentional).
The following is how to use testdouble.js:
<code class="language-javascript">// 以下是查看函数调用的参数的方法: 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();</code>
testdouble uses a simpler language. We "replace" the function instead of "stub" it. We require the testdouble "interpretation" function to get information from it. Apart from that, it's quite similar to Sinon so far.
This also extends to creating "anonymous" test stand-in:
<code class="language-javascript">var x = sinon.stub();</code>
and
<code class="language-javascript">var x = td.function();</code>
Sinon's spies and stubs have attributes that provide more information about them. For example, Sinon provides attributes such as stub.callCount
and stub.args
. In the case of testdouble, we get this information from td.explain
:
<code class="language-javascript">// 我们也可以为测试替身命名 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 } */</code>
One of the biggest differences is related to how stubs are set and verification is done. With Sinon, you can link commands after the stub and use assertions to verify the result. testdouble.js just show you how you want to call a function—or how to "rehearse" a function call.
<code class="language-javascript">var x = sinon.stub(); x.withArgs('hello', 'world').returns(true); var y = sinon.stub(); sinon.assert.calledWith(y, 'foo', 'bar');</code>
and
<code class="language-javascript">var x = td.function(); td.when(x('hello', 'world')).thenReturn(true); var y = td.function(); td.verify(y('foo', 'bar'));</code>
This makes the testdouble's API easier to understand, because you don't need to know what actions can be linked at when.
At a high level, both libraries are quite similar. But what about common testing tasks you might need to perform in a real project? Let's see where some differences begin to appear.
The first thing to note is that testdouble.js does not have the concept of "spy". While Sinon.js allows us to replace function calls to get information from them while preserving the default behavior of the function, this is simply not possible in testdouble.js. When you replace the function with testdouble, it always loses its default behavior.
But this is not necessarily a problem. The most common usage of spies is to use them to verify that callbacks are called, which is easy to accomplish with td.function
:
<code class="language-javascript">var spy = sinon.spy(); myAsyncFunction(spy); sinon.assert.calledOnce(spy);</code>
and
<code class="language-javascript">var spy = td.function(); myAsyncFunction(spy); td.verify(spy());</code>
While it's not a big deal, it's still important to note this difference between the two libraries, otherwise you might be surprised if you expect to be able to use spies in testdouble.js in some more specific way.
The second difference you will encounter is that testdouble is stricter on input.
Sinon's stubs and assertions allow you to be inaccurate with the provided parameters. This is easiest to illustrate by example:
<code class="language-javascript">var stub = sinon.stub(); stub.withArgs('hello').returns('foo'); console.log(stub('hello', 'world')); // 输出:'foo' sinon.assert.calledWith(stub, 'hello'); // 没有错误</code>
and
<code class="language-javascript">// 以下是查看函数调用的参数的方法: 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();</code>
By default, Sinon does not care how many extra parameters are provided to the function. Although it provides functions such as sinon.assert.calledWithExactly
, it is not recommended as the default value in the documentation. Functions like stub.withArgs
also do not have "exactly" variants.
Testdouble.js, on the other hand, requires the specified exact parameters by default. This is by design. The idea is that if the function is provided with some other parameters that are not specified in the test, this may be an error and should fail the test.
Arbitrary parameters can be specified in testdouble.js, but this is not the default value:
<code class="language-javascript">// 以下是查看函数调用的参数的方法: 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();</code>
Use ignoreExtraArgs: true
, the behavior is similar to Sinon.js.
While using Sinon.js's Promise is not complicated, testdouble.js has built-in methods to return and reject Promise.
<code class="language-javascript">var x = sinon.stub();</code>
and
<code class="language-javascript">var x = td.function();</code>
Note: You can use sinon-as-promised to include similar convenient functions in Sinon 1.x. Sinon 2.0 and newer versions include Promise support in the form of stub.resolves
and stub.rejects
.
Sinon and testdouble both provide an easy way to make the stub function callbacks. However, there are some differences in how they work.
Sinon uses stub.yields
to make the stub call the first function received as a parameter.
<code class="language-javascript">// 我们也可以为测试替身命名 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 } */</code>testdouble.js defaults to Node style mode, where the callback is assumed to be the
last parameter. You don't have to specify it when rehearsing the call:
<code class="language-javascript">var x = sinon.stub(); x.withArgs('hello', 'world').returns(true); var y = sinon.stub(); sinon.assert.calledWith(y, 'foo', 'bar');</code>What makes the callback support of testdouble more powerful is that you can easily define the behavior of scenarios with multiple callbacks or different orders of callbacks.
Suppose we want to call
…callback1
<code class="language-javascript">var x = td.function(); td.when(x('hello', 'world')).thenReturn(true); var y = td.function(); td.verify(y('foo', 'bar'));</code>Note that we pass
as a parameter to the function in td.callback
. This tells testdouble which parameter we want to use as the callback. td.when
<code class="language-javascript">var spy = sinon.spy(); myAsyncFunction(spy); sinon.assert.calledOnce(spy);</code>In this case, we use
instead of callsArgWith
. We have to provide a specific index of the call to make it work, which can be a bit cumbersome, especially on functions with many parameters. yields
two callbacks with certain values?
<code class="language-javascript">var spy = td.function(); myAsyncFunction(spy); td.verify(spy());</code>With Sinon, this is simply impossible. You can link multiple calls to
, but it will only call one of them. callsArgWith
, testdouble also allows you to replace the entire module. td.replace
<code class="language-javascript">var stub = sinon.stub(); stub.withArgs('hello').returns('foo'); console.log(stub('hello', 'world')); // 输出:'foo' sinon.assert.calledWith(stub, 'hello'); // 没有错误</code>If we want to replace it with testdouble, we can use
, for example...td.replace('path/to/file')
<code class="language-javascript">// 以下是查看函数调用的参数的方法: 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();</code>
Although Sinon.js can replace member functions of an object, it cannot replace modules like this. To do this when using Sinon, you need to use another module, such as proxyquire or rewire.
<code class="language-javascript">// 以下是查看函数调用的参数的方法: 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();</code>
Another thing you need to note about module replacement is that testdouble.js will automatically replace the entire module. If it is a function export like the example here, it replaces the function. If it is an object containing multiple functions, it replaces all of them. Constructors and ES6 classes are also supported. Both proxyquire and rewire require that you specify individually what and how to replace.
If you use Sinon's emulation timer, emulation XMLHttpRequest, or emulation server, you will notice that they do not exist in the testdouble.
The emulation timer can be used as a plugin, but the XMLHttpRequests and Ajax functions need to be handled in different ways.
A simple solution is to replace the Ajax function you are using, e.g. $.post
:
<code class="language-javascript">var x = sinon.stub();</code>
A common stumbling block for beginners in Sinon.js is often clean up spies and stubs. Sinon offers three different ways to do this, which doesn't help much.
<code class="language-javascript">var x = td.function();</code>or:
<code class="language-javascript">// 我们也可以为测试替身命名 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 } */</code>or:
<code class="language-javascript">var x = sinon.stub(); x.withArgs('hello', 'world').returns(true); var y = sinon.stub(); sinon.assert.calledWith(y, 'foo', 'bar');</code>Usually, it is recommended to use sandbox and
methods, otherwise it is easy to accidentally leave stubs or spies, which may cause problems with other tests. This can lead to hard-to-trace cascade failures. sinon.test
. The recommended method is to call it in the td.reset()
hook: afterEach
<code class="language-javascript">var x = td.function(); td.when(x('hello', 'world')).thenReturn(true); var y = td.function(); td.verify(y('foo', 'bar'));</code>This greatly simplifies the setup of the test stand-in and clean up after testing, reducing the possibility of difficult-to-track errors.
Pros and cons
Let's talk about Sinon.js first. It provides some more additional features than testdouble.js and some aspects of it are easier to configure. This provides some higher flexibility for it in more special testing scenarios. Sinon.js also uses the language of people who are more familiar with other languages—concepts such as spies, stubs and mocks exist in different libraries and has also been discussed in test-related books.
The disadvantage is that it increases complexity. While its flexibility allows experts to do more things, it means that certain tasks are more complex than in testdouble.js. It may also have a steeper learning curve for those new to test the concept of a stand-in. In fact, even someone as familiar with it as me may have a hard time explaining in detail some of the differences between
and sinon.stub
! sinon.mock
testdouble.js selected a simpler interface. Most of its content is fairly simple and easy to use and feels better suited to JavaScript, while Sinon.js sometimes feels like it's designed for other languages. Thanks to this and some of the design principles, it is easier for beginners to get started, and even experienced testers will find many tasks easier to complete. For example, testdouble uses the same API to set up test stand-ins and verify results. It may also be less prone to errors due to its simpler cleaning mechanism.
testdouble The biggest problem is caused by some of its design principles. For example, a complete lack of spies may make it impossible for some people who prefer to use spies over stubs. This is largely a question of opinion and you may not find the problem at all. Besides that, although testdouble.js is an updated library, it is providing some serious competition for Sinon.js.
The following is a comparison by function:
功能 | Sinon.js | testdouble.js |
---|---|---|
间谍 | 是 | 否 |
存根 | 是 | 是 |
延迟存根结果 | 否 | 是 |
模拟 | 是 | 是1 |
Promise 支持 | 是(在 2.0 中) | 是 |
时间辅助函数 | 是 | 是(通过插件) |
Ajax 辅助函数 | 是 | 否(改为替换函数) |
模块替换 | 否 | 是 |
内置断言 | 是 | 是 |
匹配器 | 是 | 是 |
自定义匹配器 | 是 | 是 |
参数捕获器 | 否2 | 是 |
代理测试替身 | 否 | 是 |
td.replace(someObject)
. stub.yield
(not to be confused with stub.yields
). Sinon.js and testdouble.js both provide a fairly similar set of features. In this regard, neither is obviously superior.
The biggest difference between the two is their API. Sinon.js is probably a little bit longer and offers many options on how to do things. This may be its pros and cons. testdouble.js has a leaner API, which makes it easier to learn and use, but due to its more arbitrary design, some may find it problematic.
Do you agree with the design principles of testdouble? If so, then there is no reason not to use it. I've used Sinon.js in many projects and I can safely say that testdouble.js does at least 95% of the work I've done in Sinon.js and the remaining 5% may be done with some simple workaround.
If you find Sinon.js difficult to use, or are looking for a more "JavaScript-style" testdouble.js might be for you as well. Even people like me who spend a lot of time learning to use Sinon, I tend to suggest trying testdouble.js and see if you like it.
However, some aspects of testdouble.js may cause headaches for those with knowledge of Sinon.js or other experienced testers. For example, the complete lack of spies may be the decisive factor. Sinon.js is still a good choice for experts and those who want the most flexibility.
If you want to learn more about how to use test stand-ins in practice, check out my free Sinon.js guide. While it uses Sinon.js, you can also apply the same techniques and best practices to testdouble.js.
Are there any problems? Comment? Are you already using testdouble.js? After reading this article, would you consider giving it a try? Please let me know in the comments below.
This article was reviewed by James Wright, Joan Yin, Christian Johansen and Justin Searls. Thanks to all SitePoint peer reviewers for getting SitePoint content to its best!
Sinon.js and Testdouble.js are both popular JavaScript test libraries, but they have some key differences. Sinon.js is known for its rich feature set, including spies, stubs, and simulations, as well as utilities for emulating timers and XHR. It is a versatile tool that can be used in conjunction with any testing framework. On the other hand, Testdouble.js is a minimalist library that focuses on providing a simple and intuitive API for testing stand-ins, which are alternatives to the parts of the system to be tested. It does not include utilities for emulating timers or XHRs, but it is designed to be easy to use and understand, so it is a great option for those who prefer a leaner method of testing.
Sinon.js and Testdouble.js can be installed through npm (Node.js package manager). For Sinon.js, you can use the command npm install sinon
. For Testdouble.js, the command is npm install testdouble
. After installation, you can use const sinon = require('sinon')
and const td = require('testdouble')
to introduce them in your test files, respectively.
Yes, Sinon.js and Testdouble.js can be used simultaneously in the same project. They are all designed to be very neatly simple and work well with other libraries. However, remember that they have overlapping features, so using them at the same time can lead to confusion. It is usually recommended to choose one of them based on your specific needs and preferences.
In Sinon.js, you can use sinon.spy()
to create spies. This function returns a spy object that records all calls made to it, including parameters, return values, and exceptions. In Testdouble.js, you can create spies using td.function()
. This function returns a test stand-alone function that records all calls, including parameters.
In Sinon.js, you can create stubs using sinon.stub()
. This function returns a stub object that behaves like a spy, but it also allows you to define its behavior, such as specifying a return value or throwing an exception. In Testdouble.js, you can create stubs using td.when()
. This function allows you to define your behavior when calling a test standby with a specific parameter.
In Sinon.js, you can use methods such as spy.called
, spy.calledWith()
, and spy.returned()
to verify spies or stubs. In Testdouble.js, you can use td.verify()
to assert whether the test standby is called in some way.
Sinon.js has a more comprehensive feature set compared to Testdouble.js. It includes utilities for emulating timers and XHR, which is very useful for testing certain types of code. It is also used more widely and has a larger community, which means more resources and support can be obtained.
Testdouble.js has a simpler and more intuitive API than Sinon.js. It is designed to be easy to use and understand, so it is a great choice for those who prefer a more streamlined test method. It also encourages good testing practices by making it difficult to abuse test stand-ins.
Yes, Sinon.js and Testdouble.js are both designed very neatly and work well with other test frameworks. They can be used with any JavaScript-enabled test framework.
Yes, Sinon.js and Testdouble.js have a lot of documentation on their official websites. There are also many tutorials, blog posts, and online courses covering in-depth content from these libraries.
The above is the detailed content of JavaScript Testing Tool Showdown: Sinon.js vs testdouble.js. For more information, please follow other related articles on the PHP Chinese website!