JavaScript testing with sinon spies and stubs

in #utopian-io8 years ago (edited)

In this tutorial you will learn by example key concepts of testing nodejs application with spies and stubs provided by sinon npm package. While the tutorial is using nodejs environment, the knowledge you gain from it can be applied for code running in browser environment as well.

This tutorial assumes basic knowledge of javascript programming language and basic command line proficiency. Feel free to ask for help in comments if you struggle with any part of the setup.

All examples use sinon version 4.2.2, which is newest version at the time of writing.

Complete code examples used in this tutorial can be found on GitHub.

Sinon primitives

When testing complicated application there is often the need to:

  1. fake external connections. This allows us to test in enclosed environment without relying on existing infrastracture, which simplifies test setup and allows us to run tests offline.
  2. assert certain internal functions were called with expected arguments, to divide test scopes in small pieces and easily pinpoint location of the error once it is present.

Sinon library gives us 3 major primitives to fulfill this requirements:

  1. spies are functions that record all actions performed on it, so that user can verify its expectations.
  2. stubs extend functionality of spies with pre-programmed behaviour.
  3. mocks extend functionality of stubs with pre-programmed expectations.

In addition, sinon provides a set of utility functions to help user in possible testing scenarios:

  1. Fake timers, as you've probably guessed from the name, manipulate process timers. This allows user to simulate passage of time to confirm accuracy of code that uses timeouts and intervals.
  2. Fake XHR is used for creating fake XMLHttpRequest and ActiveXObject implementations for testing code in front-end environments.
  3. Assertions and matchers provide syntactic sugar for verification of properties of spies/stubs.
  4. Sandboxes are containers for a set of spies/stubs. They provide a convinient way to set up tests and clean up environment afterwards.
  5. createStubInstance is an utility function to stub all functions on the function prototype, which means it works with ES6 classes as well.

Let's go ahead to see some of this tools in action

Setting up environment

Make sure you have newest versions of nodejs and npm isntalled on your system. Create a new directory for this tutorial, navigate there, and initialize new npm project.

mkdir ./sinon_tutorial
cd ./sinon_tutorial
npm init

Npm will ask you set of questions about your new project. You can accept all the default values or enter your own. Answer yes once npm asks you for confirmation.

ScreenShot.png

It's time to create an app that we are going to test. Open your favourite code editor and create following two functions:

'use strict';

/* simple addition function */
function add(a, b) {
  return a + b;
}

/* asyncronous wrapper for addition function */
function addAsync(a, b, cb) {
  const result = add(a, b);
  return cb(null, result);  // usually the first argument to callback function is an error or null if there is no error
}

module.exports = { add, addAsync };

Save the file with name index.js in sinon_tutorial folder. You can verify your functions are working correctly by hand in node repl (use node command to start it):

$ node
> const funcs = require('.');        // export index.js
undefined
> funcs.add(1, 2)                    // call syncronous function
3                                    // function returns correct value
> funcs.addAsync(1, 2, console.log)  // passing console.log as a callback
null 3                               // arguments passed to console.log
undefined                            // function itself returns undefined
>

Testing

Now let's install dependencies we need for testing. There are three: mocha, chai and sinon itself. Mocha is a widely used javascript test framework, that will handle running tests and gathering results for us. Chai package handles assertions and expectations. We will use --save-dev key to the npm command to indicate that this dependencies should only be installed in development environment, and not in production.

$ npm install mocha chai sinon --save-dev
npm WARN [email protected] No description
npm WARN [email protected] No repository field.

+ [email protected]
+ [email protected]
+ [email protected]
added 43 packages in 1.921s

Next, we will create the test file test.js:

'use strict';

const { expect } = require('chai');
const sinon = require('sinon');
const funcs = require('.');

describe('Test index.js', () => {
  describe('add', () => {
    it('calculates sum', () => {
      expect(funcs.add(1, 2)).to.equal(3);
    });
  });
});

Just like that we wrote our first test case, testing the syncronus part of our library. We can now run it and see the result:

$ ./node_modules/.bin/mocha test.js


  Test index.js
    add
      ✓ calculates sum


  1 passing (8ms)

Let's also add the test for the second function. Doing so is simple enough, we just need to add another describe block with it block inside:

  describe('addAsync', () => {
    it('calculates sum asyncronously', (done) => {
      funcs.addAsync(1, 2, (error, result) => {
        expect(result).to.equal(3);
        done();
      });
    });
  });

As you can see, because of asyncronous nature of addAsync the test is slightly more complicated. An assertion is now called inside of the callback function, and we use done callback provided by mocha framework to finish test execution.

The test does it's job and verifies that addAsync works as expected. But it also internally relies on add function, and if somebody introduces changes to the code that break add function, addAsync function will fail as well. It might not be a problem when we work with just two functions, but when project grows to include hundreds of tests it would force developer to spend valuable time on pinpointing percise location of the problem. As such, we need to limit the scope of addAsync test to only run relevant code. To achieve this, we will fake working add function with sinon.

  describe('addAsync', () => {
    beforeEach('fake add function', () => sinon.stub(funcs, 'add').yields(null, 3));
    afterEach('reset add function', () => funcs.add.reset());
    after('restore add function', () => funcs.add.restore());

    it('calculates sum asyncronously', (done) => {
      funcs.add(1, 2, (error, result) => {
        expect(result).to.equal(3);
        done();
      });
    });
  });

Now both tests are passing as before, but if we were to deliberately sabotage our add function buy making it return a + b + 1 instead of a + b only one of them breaks, and we can instantly see the problem from the mocha output:

./node_modules/.bin/mocha test.js


  Test index.js
    add
      1) calculates sum
    addAsync
      ✓ calculates sum asyncronously


  1 passing (16ms)
  1 failing

  1) Test index.js
       add
         calculates sum:

      AssertionError: expected 4 to equal 3
      + expected - actual

      -4
      +3

      at Context.it (test.js:10:34)

With stubbed add function returning value immidiately, it's now possible to simplify addAsync test by using spy as a callback. Keep in mind that it only works because addAsync does not contain non-blocking functions. To use this trick you need to make sure all non-blocking functions inside the function you're testing are properly stubbed, turning asyncronous function into syncronous one. If that's not the case assertion might fail.

    it('calculates sum asyncronously', () => {
      const spy = sinon.spy();
      funcs.addAsync(1, 2, spy);
      sinon.assert.calledWith(spy, null, 3);
    });

New version of the test function is much shorter, concise and easier to read than an old one.

Sinon spies and mocks are powerfull tools in JavaScript testing environment. I hope this tutorial helped you understand how they can be helpful and in which situation to use them.



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Hey @laxam I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

You got a 19.23% upvote from @inciter courtesy of @laxam!

You got a 15.02% upvote from @steembloggers courtesy of @laxam!

You got a 3.33% upvote as a Recovery Shot from @isotonic, currently working as a funding tool, courtesy of @laxam!

  • Image from pngtree.com

@isotonic is the Bid Bot of the @runningproject community.
Earnings obtained by this bot, after paying to the delegators, are fully used to increase the SP of the @runningproject from which all affiliated members are benefited.
Check @runningproject posts in order to know further about.

you do know that utopian is skimming your rewards with 25% ?

they take 25% of your rewards !!

don't believe me ? look here https://steemd.com/utopian-io/@laxam/javascript-testing-with-sinon-spies-and-stubs

under : beneficiaries you see that they keep 25% of your earnings and send it to @utopian.pay

Congratulations! This post has been upvoted from the communal account, @minnowsupport, by laxam from the Minnow Support Project. It's a witness project run by aggroed, ausbitbank, teamsteem, theprophet0, someguy123, neoxian, followbtcnews, and netuoso. The goal is to help Steemit grow by supporting Minnows. Please find us at the Peace, Abundance, and Liberty Network (PALnet) Discord Channel. It's a completely public and open space to all members of the Steemit community who voluntarily choose to be there.

If you would like to delegate to the Minnow Support Project you can do so by clicking on the following links: 50SP, 100SP, 250SP, 500SP, 1000SP, 5000SP.
Be sure to leave at least 50SP undelegated on your account.

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Coin Marketplace

STEEM 0.04
TRX 0.33
JST 0.083
BTC 64095.69
ETH 1726.16
USDT 1.00
SBD 0.42