Testing Sinon - AutolabJS/autolabcli GitHub Wiki

Sinon is a standalone test spies, stubs and mocks for JavaScript. Sinon helps eliminate complexity in tests by allowing you to easily create so called test-doubles. This is useful in unit tests where we can mock the dependencies of the module under test and verify if the module is performing as expected, given its dependencies are all working as expected.

Installation

Install sinon as dev-dependency using npm -

npm install sinon --save-dev

Test Doubles

Sinon splits test-doubles into three types:

  • Spies, which offer information about function calls, without affecting their behavior
  • Stubs, which are like spies, but completely replace the function. This makes it possible to make a stubbed function do whatever you like — throw an exception, return a specific value, etc
  • Mocks, which are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre-programmed expectations.

We use only mocks in unit tests. Both mocks and spies are used in integration tests. Stubs should be used only when we need to stub a function that is computationally intensive and would take a few seconds to compute.

Note:

Ideally, to mock the output provided by console.log(), sinon mocks must be used. But, it is observed that doing so produces erratic behavior. Hence the log() function of console is stubbed out using sinon stub. This is the only place where stubs are used in unit tests. An example of how to do so is present in the stubs section of this page.

Unit Tests

Only Sinon mocks are to be used in unit tests. This is because we need the test doubles in unit tests to have pre-programmed behaviour as well as pre-programmed expectations. Such a pre-programmed behaviour would isolate the methods under test from all the dependencies.

Mocks

Mocks are fake methods with pre-programmed behaviour as well as pre-programmed expectations. Mocks are primarily useful to pre-program behaviour and expectations on any number of functions of a single object.

To create a mock -

const myMock = sinon.mock(obj);

This creates a mock for the provided object. It does not change the object, but returns a mock object to set expectations on the object’s methods.

To create an expectation -

const myExpectation = myMock.expects("method");

This overrides obj.method with a mock function and returns that function.

To verify the created expectations -

myMock.verify();

To restore all the mocked method -

myMock.restore();

An Example - Assume we have a Database object and we want to mock save function of it. This can done done by -

  const info = { name: 'test' };
  const expectedUser = {
    nameLowercase: info.name.toLowerCase()
  };
  var mockDatabase = sinon.mock(Database);
  mockDatabase.expects('save').once().withArgs(expectedUser);

  setupNewUser(info); // Which calls Database.save() with expectedUser

  mockDatabase.verify();
  mockDatabase.restore();

This stubs the save function of the database, and expects the save function to have been called once with expectedUser.

Note that we define our expectations up front. Normally, the expectations would come last in the form of an assert function call. With a mock, we define it directly on the mocked function, and then only call verify in the end.

Sinon Sandboxes

Sandboxes removes the need to keep track of every fake created, which greatly simplifies cleanup. Since we might need to stub global object like console, it is better to do it inside the sandbox and restore the stubs after each test.

To create a sandbox -

const sandbox = sinon.createSandbox();

An Example -

Assume we have a function which does some processing and prints result to the terminal.

printResult(arg1, arg2) {
    let result = arg1 + arg2;
    console.log(result);
}

We can stub the log method of console, to check if the passed result is as expected -

const sandbox = sinon.createSandbox();

describe('myAPI.hello method', function () {
    
    let mockConsole;
    beforeEach(function () {
        // stub out the `hello` method
        mockConsole = sandbox.mock(console);
    });

    afterEach(function () {
        // completely restore all fakes created through the sandbox
        sandbox.restore();
    });

    it('should be called once', function () {
        mockConsole.expects('log').withExactArgs(3);
        printResult(1, 2);
        mockConsole.verify();
    });
});

Integration Tests

Spies are used along with mocks in integration tests. This is because we want to peek into the properties of a function call during the actual execution of that function using spies.

Spies

Spies are the simplest part of Sinon, and other functionality builds on top of them. The primary use for spies is to gather information about function calls. You can also use them to help verify things, such as whether a function was called or not and if the function has been called with right parameters. Sinon spies just watch over the method under test. The method executes with its original functionality.

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

//Create a spy for the setName function
const setNameSpy = sinon.spy(user, 'setName');

//Now, any time we call the function, the spy logs information about it
user.setName('Darth Vader');

expect(setNameSpy).to.have.been.calledWith('Darth Vader'); //using sinon-chai

//Important final step - remove the spy
setNameSpy.restore();

Stubs

Stubs should be avoided in the project. They should be used only when we need to stub a function that is computationally intensive and would take a few seconds (more than 10 seconds) to compute.

Stubs have all the functionality of spies, but instead of just spying on what a function does, a stub completely replaces it. In other words, when using a spy, the original function still runs, but when using a stub, it doesn’t.

To create an anonymous stub -

const myStub = sinon.stub();

This anonymous function can be passed to another function which expects a callback function.

Assume we have to stub myMethod function of myObj. We can stub an existing function of an object -

const myStub = sinon.stub(myObj, 'myMethod');

This stubs the myMethod function of myObj. This means that we can use myStub as a spy but during the execution of the test, the original function body is not executed.

To call a fake function instead of an existing function -

const myStub = stub(myObj, 'myMethod').callsFake(myFakeMethod);

Here we are faking the myMethod function of myObj object with myFakeMethod. So, when the test runs, myFakeMethod runs instead of myMethod whenever its called.

We can stub a function to return a particular value when called with certain arguments -

const addStub = sinon.stub(myObj, 'add').withArgs(1,2).returns(3);

This means that when add() function of myObj is called with arguments 1,2, it should return 3.

To restore the stubbed functions -

myStub.restore();

Example -


const myObj = {
  printOutput = () => {
   console.log('Hello World');
  }
}

const logStub = sinon.stub(console, 'log');
myObj.printOutput();
logStub.should.have.been.calledWith('Hello World');
logStub.restore();

Here we are testing the printOutput() function of myObj object to work as expected. We stub the console.log() function because we just want to know if it has been called with the right argument and we do not want it to actually print anything to the terminal during the test.

Note

Please note that it is important to restore all the test doubles created using spy. This is because sinon creates a wrapper around the object, which remains throughout the scope of the object. This is particularly undesirable when we create test doubles for global objects.

References