Home > Web Front-end > JS Tutorial > Sinon Tutorial: JavaScript Testing with Mocks, Spies & Stubs

Sinon Tutorial: JavaScript Testing with Mocks, Spies & Stubs

Joseph Gordon-Levitt
Release: 2025-02-18 10:13:13
Original
711 people have browsed it

Sinon Tutorial: JavaScript Testing with Mocks, Spies & Stubs

This article was reviewed by Mark Brown and Marc Towler. Thanks to all SitePoint peer reviewers for getting SitePoint content to its best!

One of the biggest obstacles when writing unit tests is how to deal with non-trivial code.

In actual projects, the code often performs various operations that make testing difficult. Ajax requests, timers, dates, access to other browser features…or databases are always fun if you are using Node.js, so are network or file access.

All of this is hard to test because you can't control them in your code. If you are using Ajax, you need a server to respond to the request so that your tests can pass. If you use setTimeout, your test will have to wait. The same is true for a database or network - you need a database with the correct data, or a network server.

Real life is not as easy as many test tutorials seem. But do you know there is a solution?

By using Sinon, we can make testing non-trivial code trivial!

Let's see how it works.

Key Points

  • Sinon Simplified Testing: Sinon.js is critical to simplifying the testing of JavaScript code involving complex operations such as Ajax calls, timers, and database interactions, because it allows these parts to be replaced with mocks, spies, and stubs.
  • Three types of test stand-ins: Sinon classifies test stand-ins as spies (collecting information about function calls); stubs (functions can be replaced to enforce specific behaviors); and mocks (ideally suitable for replacing and asserting behaviors of entire objects ).
  • Practical Cases: Sinon is especially useful in unit testing scenarios where external dependencies can complicate or slow down testing, such as external API calls or time-based functions.
  • Integration and Setup: Sinon can be easily integrated into Node.js and browser-based testing environments, enhancing its versatility and ease of use in a variety of JavaScript applications.
  • Enhanced Assertions: Sinon provides enhanced assertion methods that generate clearer error messages, improving the debugging process during test failures.
  • Best Practice: Use sinon.test() Packaging test cases ensures that the test stand-in is properly cleaned, prevents side effects in other tests, and reduces potential errors in the test suite.

What makes Sinon so important and useful?

In short, Sinon allows you to replace the difficult part of testing with the one that makes testing simple.

When testing a piece of code, you do not want it to be affected by any factors outside the test. If some external factors affect the test, the test becomes more complex and may fail randomly.

How do you do it if you want to test the code that makes Ajax calls? You need to run a server and make sure it provides the exact response you need for your tests. Setting up is complicated and makes writing and running unit tests difficult.

What if your code depends on time? Suppose it waits for a second before performing some action. What to do now? You can use setTimeout in your test to wait for a second, but this will slow down the test. Imagine if the interval is longer, for example, five minutes. I guess you probably don't want to wait five minutes every time you run your tests.

By using Sinon we can solve both of these problems (and many others) and eliminate complexity.

How does Sinon work?

Sinon helps remove complexity in testing by allowing you to easily create so-called test stand-in.

As the name suggests, the test stand-in is a replacement for the code snippets used in the test. Looking back at the Ajax example, we will not set up the server, but instead replace the Ajax call with the test stand-in. For the time example, we will use a test stand-in to allow us to "moving time forward".

This may sound a little strange, but the basic concept is simple. Because JavaScript is very dynamic, we can take any function and replace it with something else. Testing a stand-in is just taking this idea a step further. With Sinon, we can replace any JavaScript function with a test stand-in, and then we can configure it to perform various operations to make testing complex things simple.

Sinon divides test stand-ins into three types:

  • Spy, providing information about function calls without affecting their behavior
  • Stub, like a spy, but completely replaces the function. This makes it possible to make the stub function do anything you like - throw exceptions, return specific values, etc.
  • Simulation, by combining spies and stubs, it makes it easier to replace the entire object

In addition, Sinon provides some other help programs, although these help programs are outside the scope of this article:

  • Fake timer, can be used to move the time forward, such as triggering setTimeout
  • Fake XMLHttpRequest and server, which can be used to forge Ajax requests and responses

With these features, Sinon allows you to solve all the difficulties caused by external dependencies in testing. If you learn the tips for using Sinon effectively, you don't need any other tools.

Installation Sinon

First, we need to install Sinon.

For Node.js test:

  1. Use npm install sinon to install sinon through npm
  2. Introduce Sinon
  3. in your tests using var sinon = require('sinon');

For browser-based testing:

  1. You can use npm install sinon to install Sinon via npm, use a CDN or download it from Sinon's website
  2. Include sinon.js in your test runner page.

Beginner

Sinon has many features, but many of them are built on itself. You know part of it, and you already know the next part. Once you understand the basics and understand what each different part does, this makes Sinon easy to use.

We usually need Sinon when our code calls a function that causes us trouble.

For Ajax, it may be $.get or XMLHttpRequest. For time, the function may be setTimeout. For databases, it might be mongodb.findOne.

To make it easier to discuss this function, I call it the dependency . The function we are testing depends on the result of another function. We can say that Sinon's basic usage pattern is to replace problematic dependencies with test stand-ins.

When testing Ajax, we replace XMLHttpRequest with a test stand-in that simulates Ajax requests
  • When testing time, we replace setTimeout with a fake timer
  • When testing database access, we can replace mongodb.findOne with a test stand-in that returns some fake data immediately
  • Let's see how it works in practice.

Spy

Spy is the easiest part of Sinon, and other features are built on it.

The main purpose of spies is to collect information about function calls. You can also use them to help verify certain things, such as whether a function is called.

Function sinon.spy returns a Spy object that can be called like a function, but also contains attributes about any calls made to it. In the example above, the firstCall property contains information about the first call, such as firstCall.args, which is the passed parameter list.
var spy = sinon.spy();

//我们可以像函数一样调用间谍
spy('Hello', 'World');

//现在我们可以获取有关调用的信息
console.log(spy.firstCall.args); //输出:['Hello', 'World']
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

Although you can create anonymous spies by calling sinon.spy without parameters, the more common pattern is to replace another function with a spy.

Replace another function with a spy and works similarly to the previous example, but with one important difference: After you have finished using spy, be sure to remember to restore the original function, as shown in the last line of the above example. Without this, your tests may behave abnormally.
var user = {
  ...
  setName: function(name){
    this.name = name;
  }
}

//为 setName 函数创建一个间谍
var setNameSpy = sinon.spy(user, 'setName');

//现在,每当我们调用该函数时,间谍都会记录有关它的信息
user.setName('Darth Vader');

//我们可以通过查看间谍对象来查看
console.log(setNameSpy.callCount); //输出:1

//重要最后一步 - 删除间谍
setNameSpy.restore();
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

Spy has many different properties that provide different information about how they are used. Sinon's spy documentation contains a complete list of all available options.

In practice, you may not use spies often. You're more likely to need a stub, but spies can be convenient, such as verifying that a callback is called:

In this example, I use Mocha as the test framework and Chai as the assertion library. If you want to learn more about these two, please refer to my previous post: Unit Testing of Your JavaScript with Mocha and Chai.
function myFunction(condition, callback){
  if(condition){
    callback();
  }
}

describe('myFunction', function() {
  it('should call the callback function', function() {
    var callback = sinon.spy();

    myFunction(true, callback);

    assert(callback.calledOnce);
  });
});
Copy after login
Copy after login
Copy after login

Sinon's assertion

Before we go on to the stub, let's take a quick detour and take a look at Sinon's assertion.

In most test cases where you use spy (and stubs), you need some way to verify the results of the test.

We can use any type of assertion to verify the result. In the previous example about callbacks, we used Chai's assert function, which ensures that the value is the true value.

var spy = sinon.spy();

//我们可以像函数一样调用间谍
spy('Hello', 'World');

//现在我们可以获取有关调用的信息
console.log(spy.firstCall.args); //输出:['Hello', 'World']
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

The disadvantage of doing this is that the error message on failure is unclear. You will only receive a prompt like "false is not true" or similar. As you might imagine, this is not very helpful in finding out where the problem lies, and you need to look at the source code of the test to figure it out. Not fun.

To solve this problem, we can include custom error messages into the assertion.

var user = {
  ...
  setName: function(name){
    this.name = name;
  }
}

//为 setName 函数创建一个间谍
var setNameSpy = sinon.spy(user, 'setName');

//现在,每当我们调用该函数时,间谍都会记录有关它的信息
user.setName('Darth Vader');

//我们可以通过查看间谍对象来查看
console.log(setNameSpy.callCount); //输出:1

//重要最后一步 - 删除间谍
setNameSpy.restore();
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

But why bother when we can use Sinon's own assertion?

function myFunction(condition, callback){
  if(condition){
    callback();
  }
}

describe('myFunction', function() {
  it('should call the callback function', function() {
    var callback = sinon.spy();

    myFunction(true, callback);

    assert(callback.calledOnce);
  });
});
Copy after login
Copy after login
Copy after login

Using Sinon's assertions like this can provide better error messages immediately. This is useful when you need to validate more complex conditions such as parameters of a function.

The following are some other useful assertions provided by Sinon:

  • sinon.assert.calledWith can be used to verify that the function was called with a specific parameter (this is probably the one I use most)
  • sinon.assert.callOrder can verify that the function is called in a specific order

Like spies, Sinon's assertion documentation contains all available options. If you prefer to use Chai, there is also a sinon-chai plugin available, which allows you to use Sinon assertions via Chai's expect or should interface.

Stub

Stubs are the preferred test stand-in because they are flexible and convenient. They have all the functions of spies, but they are more than just the role of monitoring functions, and the stub completely replaces it. In other words, when using spy, the original function still runs, but when using stubs, it doesn't run.

This makes stubs very suitable for many tasks such as:

  • Replace Ajax or other external calls that make the tests slow and difficult to write
  • Trigger different code paths according to function output
  • Test exceptions, such as what happens when an exception is thrown?

The way we can create stubs is similar to that of spies...

assert(callback.calledOnce);
Copy after login

We can create anonymous stubs like spies, but stubs become very useful when you replace existing functions with stubs.

For example, if we have some code that uses the Ajax function of jQuery, it is difficult to test it. The code sends a request to any server we configure, so we need to make it available, or add a special case to the code so that it doesn't do it in a test environment - that's a big taboo. You should hardly include test-specific cases in your code.

Instead of turning to bad practices, we can use Sinon and replace the Ajax feature with a stub. This makes testing it trivial.

This is a sample function we will test. It takes the object as a parameter and sends it to a predefined URL via Ajax.

var spy = sinon.spy();

//我们可以像函数一样调用间谍
spy('Hello', 'World');

//现在我们可以获取有关调用的信息
console.log(spy.firstCall.args); //输出:['Hello', 'World']
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

Usually, testing this will be difficult due to Ajax calls and predefined URLs, but if we use stubs, it will become easy.

Suppose we want to make sure that the callback function passed to saveUser is called correctly after the request is completed.

var user = {
  ...
  setName: function(name){
    this.name = name;
  }
}

//为 setName 函数创建一个间谍
var setNameSpy = sinon.spy(user, 'setName');

//现在,每当我们调用该函数时,间谍都会记录有关它的信息
user.setName('Darth Vader');

//我们可以通过查看间谍对象来查看
console.log(setNameSpy.callCount); //输出:1

//重要最后一步 - 删除间谍
setNameSpy.restore();
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

Here, we replace the Ajax function with a stub. This means that the request is never sent, we don't need a server or anything - we have complete control over what's going on in the test code!

Because we want to make sure that the callback we pass to saveUser is called, we will instruct the stub yield. This means that the stub will automatically call the first function passed to it as a parameter. This simulates the behavior of $.post, which calls the callback after the request is completed.

In addition to the stub, we also created a spy in this test. We can use normal functions as callbacks, but using spies can easily verify the results of the test using Sinon's sinon.assert.calledOnce assertion.

In most cases, when you need stubs, you can follow the same basic pattern:

  • Find the function in question, such as $.post
  • View how it works so you can mock it in your tests
  • Create a stub
  • Set the stub to have the desired behavior in the test

Stubs do not need to simulate each behavior. The only behavior required for testing is necessary and anything else can be omitted.

Another common usage of

stubs is to verify that functions are called with a specific parameter set.

For example, for our Ajax function, we want to make sure that the correct value is sent. Therefore, we can have something like the following:

function myFunction(condition, callback){
  if(condition){
    callback();
  }
}

describe('myFunction', function() {
  it('should call the callback function', function() {
    var callback = sinon.spy();

    myFunction(true, callback);

    assert(callback.calledOnce);
  });
});
Copy after login
Copy after login
Copy after login

Similarly, we created a stub for $.post() , but this time we didn't set it to yield. This test doesn't care about callbacks, so it's unnecessary to make it yield.

We set up some variables to contain the expected data—the URL and parameters. Setting such variables is a good habit because it allows us to see the requirements of the test at a glance. It also helps us set user variables without duplicate values.

This time we used the sinon.assert.calledWith() assertion. We pass the stub as its first parameter, because this time we want to verify that the stub is called with the correct parameter.

There is another way to test Ajax requests in Sinon. This is done by using Sinon's fake XMLHttpRequest feature. We won't go into details here, but if you want to understand how it works, see my article on Ajax testing using Sinon's fake XMLHttpRequest.

Simulation

Simulation is a different approach than stubs. If you've heard of the term "simulated objects", that's the same thing - Sinon's simulations can be used to replace entire objects and change their behavior, similar to stub functions.

If you need to stub multiple functions from a single object, they are mainly useful. If you only need to replace a single function, the stub is easier to use.

You should be careful when using simulation! Due to their power, it's easy to make your tests too specific – too many and too specific things – which can inadvertently make your tests vulnerable.

Unlike spies and stubs, simulations have built-in assertions. You can pre-defined the expected result by telling the mock object what needs to happen and then calling the verification function at the end of the test.

Suppose we use store.js to save the content to localStorage, and we want to test the functions related to it. We can use simulation to help test it as follows:

var spy = sinon.spy();

//我们可以像函数一样调用间谍
spy('Hello', 'World');

//现在我们可以获取有关调用的信息
console.log(spy.firstCall.args); //输出:['Hello', 'World']
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

When using simulation, we use a smooth call style to define the expected call and its results, as shown above. This is the same as using assertion to validate the test results, except that we predefined them and to verify them, we call storeMock.verify() at the end of the test.

In Sinon's mock object term, calling mock.expects('something') creates a expect. That is, the method mock.something() is expected to be called. Each expectation supports the same functions as spy and stubs in addition to emulating specific functions.

You may find that using stubs is usually easier than using simulations – it's totally OK. Simulation should be used with caution.

For a complete list of simulated specific functions, check out Sinon's simulation documentation.

Important best practices: Use sinon.test()

Sinon has an important best practice that should be kept in mind when using spies, stubs, or simulations.

If you replace an existing function with a test stand-in, use sinon.test().

In the previous example, we use stub.restore() or mock.restore() to clean up the contents after using them. This is necessary because otherwise the test stand-in will remain in place and may negatively affect other tests or lead to errors.

But it is problematic to use the restore() function directly. The function being tested may cause an error and end the test function before calling restore()!

We have two ways to solve this problem: we can wrap the entire content in a try catch block. This allows us to put the restore() call in the finally block, making sure that it will run no matter what happens.

Or, a better way is to wrap the test function using sinon.test()

var user = {
  ...
  setName: function(name){
    this.name = name;
  }
}

//为 setName 函数创建一个间谍
var setNameSpy = sinon.spy(user, 'setName');

//现在,每当我们调用该函数时,间谍都会记录有关它的信息
user.setName('Darth Vader');

//我们可以通过查看间谍对象来查看
console.log(setNameSpy.callCount); //输出:1

//重要最后一步 - 删除间谍
setNameSpy.restore();
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
In the example above, note that the second parameter of it() is wrapped in sinon.test() . The second thing to note is that we use this.stub() instead of sinon.stub().

Use sinon.test() wrapping test allows us to use Sinon's

sandbox function, allowing us to create spies and stubs through this.spy(), this.stub() and this.mock() and simulation. Any test stand-in created using the sandbox will be automatically cleaned up.

Note that our example code above does not have stub.restore() - it is unnecessary since the test is sandboxed.

If you use sinon.test() whenever possible, you can avoid the problem that the test starts to fail randomly due to an early test without cleaning up its test stand-alone due to errors.

Sinon is not a magic

Sinon performs many operations and can sometimes be difficult to understand how it works. Let's take a look at some simple JavaScript examples of how Sinon works so we can better understand how it works internally. This will help you use it more efficiently in different situations.

We can also create spies, stubs and simulations manually. The reason we use Sinon is that it makes tasks trivial – creating them manually can be very complex, but let’s see how it works to understand what Sinon does.

First of all, spy is essentially a function wrapper:

var spy = sinon.spy();

//我们可以像函数一样调用间谍
spy('Hello', 'World');

//现在我们可以获取有关调用的信息
console.log(spy.firstCall.args); //输出:['Hello', 'World']
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

We can easily get spy functionality using custom functions. But note that Sinon's spies offer a wider range of features—including assertion support. This makes Sinon more convenient.

What about the stub?

To create a very simple stub, you can simply replace a function with a new function:

var user = {
  ...
  setName: function(name){
    this.name = name;
  }
}

//为 setName 函数创建一个间谍
var setNameSpy = sinon.spy(user, 'setName');

//现在,每当我们调用该函数时,间谍都会记录有关它的信息
user.setName('Darth Vader');

//我们可以通过查看间谍对象来查看
console.log(setNameSpy.callCount); //输出:1

//重要最后一步 - 删除间谍
setNameSpy.restore();
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

However, Sinon's stubs offer several advantages:

  • They contain full spy features
  • You can easily restore the original behavior using stub.restore()
  • You can assert against Sinon stubs

Simulations simply combine the behavior of spies and stubs, so that their functionality can be used in different ways.

Even though Sinon sometimes looks like it's done a lot of "magic", in most cases, this can be easily done with your own code. Sinon is much more convenient to use, rather than having to write its own library for that purpose.

Conclusion

Testing real-life code sometimes seems too complicated and can easily give up completely. But with Sinon, testing almost any type of code becomes a breeze.

Just remember the main principle: If the function makes your test difficult to write, try replacing it with a test stand-in. This principle applies no matter what the function performs.

Want to know how to apply Sinon in your own code? Visit my website and I'll send you my free real world Sinon guide that includes three real world examples of Sinon best practices and how to apply it in different types of testing situations!

FAQs about Sinon.js testing (FAQ)

What is the difference between mocks, spies and stubs in Sinon.js?

In Sinon.js, mocks, spies, and stubs have different uses. A spy is a function that records all called parameters, return values, the value of this, and the exception thrown (if any). They can be used to track function calls and responses. The stub is similar to a spy, but has pre-programmed behavior. They also record information about how they are called, but unlike spies, they can be used to control the behavior of methods to force methods to throw errors or return specific values. Simulation is a false method with pre-programmed behavior (such as stubs) as well as pre-programmed expectations (such as spy).

How to use Sinon.js for unit testing in JavaScript?

Sinon.js is a powerful tool for creating spies, stubs, and mocks in JavaScript testing. To use it, you first need to include it in your project by using script tags in your HTML or installing it via npm. Once included, you can use its API to create and manage spies, stubs, and mocks. These can then be used to isolate the code you are testing and make sure it works as expected.

How to create a spy in Sinon.js?

Creating a spy in Sinon.js is simple. You just need to call the sinon.spy() function. This will return a spy function that you can use in your tests. The spy will record information about how to call it, which you can then check in your tests. For example, you can check how many times a spy is called, what parameters are used to call it, and what it returns.

How to create stubs in Sinon.js?

To create a stub in Sinon.js, you need to call the sinon.stub() function. This will return a stub function that you can use in your tests. The stub behaves like a spy, recording information about how to call it, but it also allows you to control its behavior. For example, you can make the stub throw an error or return a specific value.

How to create a mock in Sinon.js?

Creating a mock in Sinon.js involves calling the sinon.mock() function. This will return a mock object that you can use in your tests. The mock object behaves like a spy, logs information about how to call it, and is similar to a stub, allowing you to control its behavior. But it also allows you to set expectations about how to call it.

How to use Sinon.js with other testing frameworks?

Sinon.js is designed to be used with any JavaScript testing framework. It provides a standalone testing framework, but it can also be integrated with other popular testing frameworks such as Mocha, Jasmine, and QUnit. The Sinon.js documentation provides examples of integration with these and other test frameworks.

How to restore a stub or spy to its original function?

If you have replaced the function with a stub or spy, you can restore the original function by calling the .restore() method on the stub or spy. This is useful if you want to clean up after testing to make sure the stub or spy doesn't affect other tests.

How to check if spy was called with specific parameters?

Sinon.js provides several ways to check how to call a spy. For example, you can use the .calledWith() method to check if a spy was called with a specific parameter. You can also use the .calledOnceWith() method to check if the spy was called only once with a specific parameter.

How to make a stub return a specific value?

You can use the .returns() method to make the stub return a specific value. For example, if you have a stub named myStub, you can return the value 'foo' by calling myStub.returns('foo').

How to make a stub throw an error?

You can use the .throws() method to make the stub throw an error. For example, if you have a stub named myStub, you can make it throw an error by calling myStub.throws() . By default, this will throw an Error object, but you can also make the error throw a specific type of error by passing the name of the error as a parameter.

The above is the detailed content of Sinon Tutorial: JavaScript Testing with Mocks, Spies & Stubs. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Articles by Author
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template