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
export const fetchData = (name: string) => {
return `Original message: I would love to rave on Crystal Fighters: ${name}`;
}
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');
});
});
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
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';
}
}
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');
});
});
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
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
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
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
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
https://stackoverflow.com/questions/31989040/can-gmock-be-used-for-stubbing-c-functions
[1] “Unit Testing - Can Gmock Be Used for Stubbing C Functions?,” Stack Overflow. [2] “Mocha - the Fun, Simple, Flexible JavaScript Test Framework.” [3] “Sinon.JS - Standalone Test Fakes, Spies, Stubs and Mocks for JavaScript. Works with Any Unit Testing Framework.”https://semaphoreci.com/community/tutorials/best-practices-for-spies-stubs-and-mocks-in-sinon-js, Dec. 2015
[4] “Best Practices for Spies, Stubs and Mocks in Sinon.Js,” Semaphore.https://github.com/mochajs/mocha-examples, May 2020
[5] “Mochajs/Mocha-Examples.”https://github.com/mochajs/mocha-examples/tree/master/packages/typescript, May 2020
[6] “Typescript Mocha Examples.”