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<string> => {
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<string> => {
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<string> => {
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.”