Typescript function stubs with Sinon.js

by: Artur Dziedziczak

May 23, 2020

Introduction

Unit tests are something we all heard of but not everyone had oportunity to see them at work ;)

In different languages unit testing can differ due to variaty of testing framework and the capapilities of the language. For example when I start reading about using stub structures in C++ [1] my brain is lagging

Thankfully this post is about JavaScript and testing it is really easy

Sinon.js/Typescript/Mocha

In this post I'll focus on unit tests made with Mocha [2] and library SinonJS [3] for Typescript environment

Mocha

Mocha is a test framework which will invoke our tests. It was written to execute tests written in JavaScript but when you look into their github [5] you will find plenty of configurations

In my case I used the basic configuration for typescript environment [6]

SinonJS

SinonJS is a library that allows you to make stubs/spies and mocks in JavaScript. On their webiste it is claimed that SinonJS Works with any unit testing framework which is true since it should not interfere with testing framework

As JavaScript object can be extendable via their prototypes something similar is happening with Sinon

To show what’s going on under the hood we need simple example

Listing 1. Function that will be stub
export const fetchData = (name: string) => {
    return `Original message: I would love to rave on Crystal Fighters: ${name}`;
}
Listing 2. Unit test which shows how Sinon stub methods
import { equal } from "assert";
import * as api from './api';
import { fetchData } from './api';
import * as sinon from 'sinon';
import { inspect } from 'util';

describe("Sinon tests mocking API", () => {
    afterEach(() => {
        sinon.restore();
    });

    it("How sinon works test", () => {
        console.log('Before stub', inspect(api, { showHidden: true }));
        sinon.stub(api, 'fetchData').returns('Mocked by Sinon');
        console.log('After stub', inspect(api, { showHidden: true }));

        equal(fetchData('test'), 'Mocked by Sinon');
        equal((api.fetchData as any).wrappedMethod('test'), 'Original message: I would love to rave on Crystal Fighters: test');
    });
});
Listing 3. Output after test is run
excercise1 git:(master) npm run test

> excercise@1.0.0 test /home/user/workspace/typescript-sinon-testing/excercise1
> mocha

  Sinon tests mocking API
Before stub {
  [__esModule]: true,
  fetchData: [Function] {
    [length]: 1,
    [name]: '',
    [prototype]: { [constructor]: [Circular] }
  }
}
After stub {
  [__esModule]: true,
  fetchData: [Function] {
    [length]: 1,
    [name]: '',
    [prototype]: functionStub { [constructor]: [Function] },
    [... here lots of utility functions ...]
    [isSinonProxy]: true,
    [called]: false,
    [notCalled]: true,
    [calledOnce]: false,
    [calledTwice]: false,
    [calledThrice]: false,
    [callCount]: 0,
    [firstCall]: null,
    [displayName]: 'fetchData',
    [wrappedMethod]: [Function] { [length]: 1, [name]: '', [prototype]: [Object] },
  }
}
    ✓ How sinon works test

As you can see Before stub fetchData had only three properties and After stub output shows that fetchData was enchanced with Sinon so now it has whole familiy of methods ready to use by library

At the end we perform two tests that don't fail on the output. That means first call of fetchData was properly changed by Sinon and the second assert also pass which means the wrappedMethod property of fetchData has the orginal function call

Everytime test finish you need to restore previous object state. If you won't do this element that you stub will have this same stub in every test case. There are different ways to restore Sinon stub but in this post I'll invoke sinon.restore after each test

afterEach(() => {
    sinon.restore();
});

Simple stubs

First example that I'll show will stub api call that returns a promise

Listing 4. Api module which remote query we want to stub
export const fetchDataFromRemoteApi = async (shouldThrow: boolean): Promise => {
    return new Promise((resolve, reject) => {
        if (shouldThrow) {
            reject('Api rejected response');
        }
        setTimeout(() => resolve('Api responded: CrystalFightersGigShouldHappenInPoland'), 1500);
    })
}

export const apiFetch = async (shouldThrown: boolean) => {
    try {
        return await fetchDataFromRemoteApi(shouldThrown);
    } catch (error) {
        return 'Error was thrown';
    }
}
Listing 5. First test cases
import { equal } from "assert";
import { apiFetch } from './api';
import * as sinon from 'sinon';
import * as api from './api';

describe("Sinon tests mocking API", () => {
    afterEach(() => {
        sinon.restore();
    });

    it("Fetch data without sinon", async () => {
        const response = await apiFetch(false);
        equal(response, 'Api responded: CrystalFightersGigShouldHappenInPoland');
    });

    it("Fetch data without sinon and promise is rejected", async () => {
        const response = await apiFetch(true);
        equal(response, 'Error was thrown');
    });

    it("Stub api with SinonJS", async () => {
        sinon.stub(api, 'fetchDataFromRemoteApi').returns(Promise.resolve('Subbed by Sinon'));
        const response = await apiFetch(false);
        equal(response, 'Subbed by Sinon');
    });

    it("Stub api with SinonJS and simulate throw Exception", async () => {
        sinon.stub(api, 'fetchDataFromRemoteApi').throws(new Error('Api Error'));
        const response = await apiFetch(false);
        equal(response, 'Error was thrown');
    });
});
Listing 6. Output from tests
excercise2 git:(master) npm run test

> excercise@1.0.0 test /home/user/workspace/typescript-sinon-testing/excercise2
> mocha



  Sinon tests mocking API
    ✓ Fetch data without sinon (1504ms)
    ✓ Fetch data without sinon and promise is rejected
    ✓ Stub api with SinonJS
    ✓ Stub api with SinonJS and simulate throw Exception


  4 passing (2s)

As you can see first test took 1.5s to finish. That means we queried our simulated api function which had timeout of 1500 miliseconds

Second one triggers promise rejection to show that this should throw Error and custom error message should be returned from catch (error) block

The third test was the one which Stub the api call with cutom response. In this case it was a different string

The last example was a bit more advanced in orter to show that you can not only modify function returns but it is also possible to trigger custom errors

Testing more complex code

Now let's see a more advanced function calls

Listing 7. Code to test
export const firstFetch = async (shouldThrow: boolean, customMessage?: string): Promise => {
    return new Promise((resolve, reject) => {
        if (shouldThrow) {
            reject({ error: 'firstFetch rejected response' });
        }
        setTimeout(() => {
            const message = customMessage ? customMessage : 'CrystalFightersGigShouldHappenInPoland'
            return resolve(`First fetch responded: ${message}`)
        }, 500);
    })
}

export const secondFetch = async (shouldThrow: boolean): Promise => {
    return new Promise((resolve, reject) => {
        if (shouldThrow) {
            reject('secondFetch rejected response');
        }
        setTimeout(() => resolve('Second fetch responded: Corona please staph, I need party'), 500);
    })
}

export const apiFetch = async (shouldThrown: boolean, customMessage?: string) => {
    try {
        const firstResponse = await firstFetch(shouldThrown, customMessage);

        if (firstResponse === 'First fetch responded: CrystalFightersGigShouldHappenInPoland') {
            const secondResponse = await secondFetch(shouldThrown);
            return secondResponse;
        }
        return firstResponse;
    } catch (error) {
        return `Error was thrown: ${error}`;
    }
}

So I made simple function call flow. Whenever first respose is returning 'First fetch responded: CrystalFightersGigShouldHappenInPoland' the secondFetch is called

In case of firstFetch, secondFetch is rejected or the error thrown, the catch (error) adds custom message

Listing 8. Unit test with flow checks
import { equal, throws } from "assert";
import { firstFetch, secondFetch, apiFetch } from './api';
import * as api from './api';
import * as sinon from 'sinon';

describe("Sinon tests mocking API", () => {

    describe('Not stubbed api calls', () => {
        it("apiFetch should return firstResponse", async () => {
            const response = await apiFetch(false, 'Custom message');
            equal(response, 'First fetch responded: Custom message');
        });

        it("apiFetch should return secondResponse as no custom message is provided", async () => {
            const response = await apiFetch(false);
            equal(response, 'Second fetch responded: Corona please staph, I need party');
        });
    });

    describe('apiFetch stub fistFech, secondFetch', () => {
        afterEach(() => {
            sinon.restore();
        });

        it("firstFetch stub return custom message", async () => {
            sinon.stub(api, 'firstFetch').returns(Promise.resolve('firstFetch Sinon stub'));
            const response = await apiFetch(false);
            equal(response, 'firstFetch Sinon stub');
        });

        it("firstFetch stub return predefined message and trigger stubbed secondFetch", async () => {
            sinon.stub(api, 'firstFetch').returns(Promise.resolve('First fetch responded: CrystalFightersGigShouldHappenInPoland'));
            sinon.stub(api, 'secondFetch').returns(Promise.resolve('secondFetch Sinon stub'));
            const response = await apiFetch(false);
            equal(response, 'secondFetch Sinon stub');
        });

        it("firstFetch stub throws errror", async () => {
            sinon.stub(api, 'firstFetch').throws('firstFetch rejected response');
            const response = await apiFetch(false);
            equal(response, 'Error was thrown: firstFetch rejected response');
        });

        it("firstFetch stub rejects promise", async () => {
            sinon.stub(api, 'firstFetch').rejects('firstFetch rejected response');
            const response = await apiFetch(false);
            equal(response, 'Error was thrown: firstFetch rejected response');
        });

        it("secondFetch stub rejects promise", async () => {
            sinon.stub(api, 'firstFetch').returns(Promise.resolve('First fetch responded: CrystalFightersGigShouldHappenInPoland'));
            sinon.stub(api, 'secondFetch').rejects('secondFetch rejected response');
            const response = await apiFetch(false);
            equal(response, 'Error was thrown: secondFetch rejected response');
        });

        it("secondFetch stub throws promise", async () => {
            sinon.stub(api, 'firstFetch').returns(Promise.resolve('First fetch responded: CrystalFightersGigShouldHappenInPoland'));
            sinon.stub(api, 'secondFetch').throws('secondFetch rejected response');
            const response = await apiFetch(false);
            equal(response, 'Error was thrown: secondFetch rejected response');
        });
    })
});

Unit tests are grouped and first group is the one which triggers real api calls. Second group is the Stub one

As you can see depending on the different stubs various flows of apiFetch were executed

Listing 9. Output from tests
excercise3 git:(master) ✗ npm run test

> excercise@1.0.0 test /home/user/workspace/typescript-sinon-testing/excercise3
> mocha



  Sinon tests mocking API
    Not stubbed api calls
      ✓ apiFetch should return firstResponse (502ms)
      ✓ apiFetch should return secondResponse as no custom message is provided (1003ms)

      ✓ firstFetch stub return custom message
      ✓ firstFetch stub return predefined message and trigger stubbed secondFetch
      ✓ firstFetch stub throws errror
      ✓ firstFetch stub rejects promise
      ✓ secondFetch stub rejects promise
      ✓ secondFetch stub throws promise


  8 passing (2s)

At the end let's look at the Mocha output. What you can see is that Not stubbed api calls take a lot of time since they simulate remote resource that needs to be queried

apiFetch stub fistFech, secondFetch on the other hand is using Sinon and it takes only 500ms to execute. It's 3 times faster and more tests were executed in this same time

Checking execution path

Last example will be checking execution path. Imagine you have a function with specyfic algorithm and you want to be sure that this function will be called with certain arguments or your it will invoke other functions particular amount of times

This can be checked in your unit tests and it's pretty simple to do so

Code that we test don't change from example 3. Only thing that is different are the tests

Listing 10. Unit tests with check of call invocations
import { equal, throws, ok } from "assert";
import { firstFetch, secondFetch, apiFetch } from './api';
import * as api from './api';
import * as sinon from 'sinon';

describe("Sinon tests mocking API", () => {
    describe('apiFetch stub fistFech, secondFetch with called amount', () => {
        afterEach(() => {
            sinon.restore();
        });

        it("firstFetch stub return custom message", async () => {
            const firstFetchStub = sinon.stub(api, 'firstFetch').returns(Promise.resolve('firstFetch Sinon stub'));
            const secondFetchStub = sinon.stub(api, 'secondFetch');
            const response = await apiFetch(false);
            equal(response, 'firstFetch Sinon stub');
            ok(firstFetchStub.calledOnce);
            ok(secondFetchStub.notCalled);
            ok(firstFetchStub.calledWith(false, undefined));
        });

        it("firstFetch stub return predefined message and trigger stubbed secondFetch", async () => {
            const firstFetchStub = sinon.stub(api, 'firstFetch').returns(Promise.resolve('First fetch responded: CrystalFightersGigShouldHappenInPoland'));
            const secondFetchStub = sinon.stub(api, 'secondFetch').returns(Promise.resolve('secondFetch Sinon stub'));
            const response = await apiFetch(false);
            equal(response, 'secondFetch Sinon stub');
            ok(firstFetchStub.calledOnce);
            ok(secondFetchStub.calledOnce);
            ok(firstFetchStub.calledWith(false));
            ok(secondFetchStub.calledWith(false));
        });

        it("firstFetch stub throws errror", async () => {
            const firstFetchStub = sinon.stub(api, 'firstFetch').throws('firstFetch rejected response');
            const secondFetchStub = sinon.stub(api, 'secondFetch');
            const response = await apiFetch(true);
            equal(response, 'Error was thrown: firstFetch rejected response');
            ok(firstFetchStub.calledOnce);
            ok(firstFetchStub.calledWith(true));
            ok(secondFetchStub.notCalled);

        });

        it("firstFetch stub rejects promise", async () => {
            const firstFetchStub = sinon.stub(api, 'firstFetch').rejects('firstFetch rejected response');
            const secondFetchStub = sinon.stub(api, 'secondFetch');
            const response = await apiFetch(true);
            equal(response, 'Error was thrown: firstFetch rejected response');
            ok(firstFetchStub.calledOnce);
            ok(firstFetchStub.calledWith(true));
            ok(secondFetchStub.notCalled);
        });

        it("secondFetch stub rejects promise", async () => {
            const firstFetchStub = sinon.stub(api, 'firstFetch').returns(Promise.resolve('First fetch responded: CrystalFightersGigShouldHappenInPoland'));
            const secondFetchStub = sinon.stub(api, 'secondFetch').rejects('secondFetch rejected response');
            const response = await apiFetch(false);
            equal(response, 'Error was thrown: secondFetch rejected response');
            ok(firstFetchStub.calledOnce);
            ok(firstFetchStub.calledWith(false));
        });

        it("secondFetch stub throws promise", async () => {
            const firstFetchStub = sinon.stub(api, 'firstFetch').returns(Promise.resolve('First fetch responded: CrystalFightersGigShouldHappenInPoland'));
            const secondFetchStub = sinon.stub(api, 'secondFetch').throws('secondFetch rejected response');
            const response = await apiFetch(false);
            equal(response, 'Error was thrown: secondFetch rejected response');
            ok(firstFetchStub.calledOnce);
            ok(firstFetchStub.calledWith(false));
            ok(secondFetchStub.calledOnce);
        });

        it("apiFetch stub with custom message", async () => {
            const firstFetchStub = sinon.stub(api, 'firstFetch').returns(Promise.resolve('First fetch responded: custom message'));
            const secondFetchStub = sinon.stub(api, 'secondFetch');
            const response = await apiFetch(false, 'custom message');

            equal(response, 'First fetch responded: custom message');
            ok(firstFetchStub.calledOnce);
            ok(firstFetchStub.calledWith(false, 'custom message'));
            ok(secondFetchStub.notCalled);
        });
    })
});

As you can see with usage of ok function it was possible to check boolean properties of stubs

Not only it's possible to check how many times api call was invoked but we can check what arguments were provided

Conclusion

As you can see it's easy to stub remote api calls with custom responses. Also it is possible to check amount of function calls and arguments passed

This not only makes your code isolated but also no aditional remote dependencies are needed to test your code. It also gives you possiblity to run your tests when you are offline

For me the biggest gain from the unit tests is over time when lots of developers make changes to your code and with all this checks you can be sure that functionality that was build still works correctly

This might not ensure that some remote API will work when you deploy your code to production but at least you will be sure that certain triggers are toggled when for example API returns unwanted data

All examples can be found on my github

Final notes

  • If you want to read more about stub/mock/spy in Sinon I suggest to use this blog post. [4] It's one of the best I found and I think even documentation of Sinon is not as good as this guys explanation.
  • Never test your code with connection to remote dependencies. This not only makes your tests slow but also you won't be able to reproduce same environment every time tests are run.
  • Try to write unit tests from the begining of the project and if you have legacy code that is not tested don't try to write all tests at once. This work should be splited and be incremental.

Sources

[1] “Unit Testing - Can Gmock Be Used for Stubbing C Functions?,” Stack Overflow. https://stackoverflow.com/questions/31989040/can-gmock-be-used-for-stubbing-c-functions

[2] “Mocha - the Fun, Simple, Flexible JavaScript Test Framework.” https://mochajs.org/

[3] “Sinon.JS - Standalone Test Fakes, Spies, Stubs and Mocks for JavaScript. Works with Any Unit Testing Framework.” https://sinonjs.org/

[4] “Best Practices for Spies, Stubs and Mocks in Sinon.Js,” Semaphore. https://semaphoreci.com/community/tutorials/best-practices-for-spies-stubs-and-mocks-in-sinon-js, Dec. 2015

[5] “Mochajs/Mocha-Examples.” https://github.com/mochajs/mocha-examples, May 2020

[6] “Typescript Mocha Examples.” https://github.com/mochajs/mocha-examples/tree/master/packages/typescript, May 2020