Introduction
Test "mocks" are objects that replace real objects while simulating their functions. A mock also has expectations about how the functions being tested will be used.
In some unit test cases we may want to combine the functionality of spies, to observe a method's behavior under call, and that of stubs, to replace a method's functionality, in ensuring that we do not make an actual function call but are still able to monitor the behavior of our target function accordingly. In such a case, we can use mocks.
In this article, we will seek to understand what mocks are and how to use them in unit tests. We'll then get hands-on experience with Sinon.js to mock an HTTP request.
This article is the third of our series on unit testing techniques with Sinon.js. We recommend that you read our previous articles on this topic as well:
- Using Stubs for Testing in JavaScript with Sinon.js
- Using Spies for Testing in JavaScript with Sinon.js
- Using Mocks for Testing in JavaScript with Sinon.js (you are here)
What are Mocks?
Mocks combine the functionality of both spies and stubs, which means that they replace the target function but at the same time provide us with the ability to observe how the function was called.
Furthermore, mocks have built-in assertions called expectations. You define the expectations of how your mocked function will be used upfront. If your mock did not satisfy its expectations, your test will fail.
For example, let's consider a function that communicates with a database to save a contact's details. With a mock, instead of a real database, our function will hit a fake database object. We can determine what kind of response it will give. We'll also state how many times the database should be called and the arguments it should be called with.
Finally, as part of the test, we verify that our database mock was called the exact amount of times we expected. We also check that it got called with only the arguments our function supposed to provide it.
Having seen what mocks are, let us now look at situations where we can employ them.
Why Use Mocks?
Mocks are useful when validating how an external dependency is used within a function. Use mocks when you are in interested in:
- Confirming that your external dependency is used at all
- Verifying that your external dependency is used correctly
- Ensuring that your function can handle different responses from external dependencies.
Imagine that you are testing a function that speaks with a third party API to get some user data. To make requests to the external API, you'll need to make a few calls to authenticate first. It's already becoming inconvenient to use the real API in tests. Additionally, you might not always have an internet connection to access the API while running your tests.
With a mock, we will return fake responses. We can now quickly test that our function behaves correctly when given the fake data in a particular format. We'll also know that our function made requests to the API with the right parameters.
Let's now look at how we can use Sinon.js to create mocks.
Using Sinon.js to Create a Mock
We'll use Sinon.js to mock a response from a JSON API that retrieves a list of photos in an album. In addition to Sinon.js, we will be using Mocha and Chai to setup and run the tests. You can read our guide our guide to learn more about them before continuing.
Setup
Create a directory called SinonMock
and move into it:
$ mkdir SinonMock
$ cd SinonMock
We will then use NPM to initialize a project to track the project files we create:
$ npm init -y
Next, we'll install Mocha and Chai as testing dependencies to run our tests, along with Sinon.js:
$ npm i mocha chai sinon --save-dev
Having completed our setup, let's mock an HTTP request.
Mocking an HTTP Call with Sinon.js
In our previous article on test spies, we spied on an HTTP request to the photo album API. We'll continue with that example for this article.
Create a file in the root of the SinonMock
directory and call it index.js
:
$ touch index.js
In the file created, enter the following code:
const request = require('request');
module.exports = {
getAlbumById: async function(id) {
const requestUrl = `https://jsonplaceholder.typicode.com/albums/${id}/photos?_limit=3`;
return new Promise((resolve, reject) => {
request.get(requestUrl, (err, res, body) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(body));
});
});
}
};
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
To recap, getAlbumById()
is a function which calls a JSON API that returns a list of photos. We provide an album ID as the function argument. We have previously explored stubbing and spying on the request.get()
method.
Now, we will mock the request
object and check if the get()
method is called once, as required, and verify if it received the correct arguments. We'll then verify that our function has the correct properties based on what was returned from our mock.
Create another file at the root of the SinonMock
directory and call it index.test.js
:
$ touch index.test.js
Open the index.test.js
file with an editor and enter the following code:
const expect = require('chai').expect;
const request = require('request');
const sinon = require('sinon');
const index = require('./index');
describe('with mock: getPhotosByAlbumId', () => {
it('should getPhotosByAlbumId', (done) => {
let requestMock = sinon.mock(request);
const myPhotos = [{
"albumId": 1,
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
},
{
"albumId": 1,
"id": 2,
"title": "reprehenderit est deserunt velit ipsam",
"url": "https://via.placeholder.com/600/771796",
"thumbnailUrl": "https://via.placeholder.com/150/771796"
},
{
"albumId": 1,
"id": 3,
"title": "officia porro iure quia iusto qui ipsa ut modi",
"url": "https://via.placeholder.com/600/24f355",
"thumbnailUrl": "https://via.placeholder.com/150/24f355"
}];
requestMock.expects("get")
.once()
.withArgs('https://jsonplaceholder.typicode.com/albums/2/photos?_limit=3')
.yields(null, null, JSON.stringify(myPhotos));
index.getAlbumById(2).then((photos) => {
expect(photos.length).to.equal(3);
photos.forEach((photo) => {
expect(photo).to.have.property('id');
expect(photo).to.have.property('title');
expect(photo).to.have.property('url');
});
requestMock.verify();
requestMock.restore();
done();
});
});
});
In our test case above, we first create a mock of the request
object using sinon.mock()
and name it requestMock
. The requestMock
object has the functions of the request
object but the functions do nothing by default.
After supplying some fixed photo data, we override the original get()
method of the request object by using Sinon.js' mock API's expect()
method.
The expect()
method takes in a single argument, which is the method of the mocked object we anticipate will be used.
The once()
method asserts that our expectation is called once. In this case the get()
method of the request object will be called exactly one time.
The withArgs()
method asserts that we expect the get()
method to be called with the array of arguments we supply to it. In our case the URL of the API.
The yields()
method puts data into the callback that our mock object accepts. In this case, our error and response are both null
but our body has a JSON response.
After this setup, we then call our getAlbumById()
method and check if the photos returned to have the correct properties.
Notice the verify()
call of the requestMock
object to confirm that our expectations were met. If expectations fail, the test will throw an exception. We then call the restore()
method to discard the mock created by our test and restore the original request object.
When you run this test, you should get the following result:
$ mocha index.test.js
with mock: getPhotosByAlbumId
✓ should getPhotosByAlbumId
1 passing (13ms)
✨ Done in 0.72s.
To confirm the behavior of our mock, let's see if the expectations fail if we change the URL we communicate with. In your index.js
file, change:
const requestUrl = `https://jsonplaceholder.typicode.com/albums/${id}/photos?_limit=3`;
To:
const requestUrl = `https://example.com`;
Now run the tests once more:
$ mocha index.test.js
Since we changed the input to the get()
method, the URL argument no longer matches what's in our test. We'll get this output when we run the test:
> mocha index.test.js
with mock: getPhotosByAlbumId
(node:85434) UnhandledPromiseRejectionWarning: ExpectationError: Unexpected call: get(https://example.com, function () {})
Expected get(https://jsonplaceholder.typicode.com/albums/2/photos?_limit=3[, ...]) once (never called)
Great! We're pretty certain that our mocks will guarantee that our function behaves as we expect!
By using a mock, we have been able to get the benefits of both spies and stubs. We were able to check that our function was called exactly once, and with the correct arguments, a benefit provided us by spies. We were also able to stub the request so that we were not making an actual HTTP call to the API, which ensured that our test ran quickly.
Conclusion
Mocks in unit testing combine the functionality of both spies and stubs by replacing functions like stubs and at the same time providing us with means of observing the functions to check how they were called, the functionality provided us by spies. Mocks then give us the benefit of verifying how our function was used in a test.
In this article, we introduced the concept of mocking in unit testing, and saw how we could mock an HTTP call. To learn more about Sinon.js mocks, you can review the official documentation of the mocks API.