Intro
Let me start by saying that, as a beginner, I found testing to be pretty tedious and complicated. I just wanted to create an app that was going to make millions and to hell with the test coverage! However, having spent the last year working across the full stack and writing a lot of unit and integration tests in the process, I am now more at peace with the testing gurus and appreciate the genuine value that tests can bring to your codebase. While it does require a slight change of perspective, testing should feel like taking a gentle stroll through the countryside away from the hustle and bustle of business logic.
Practical
Diving straight into our scenario then. We simply want to test our function that fetches UK bank holiday dates from this URL - https://www.gov.uk/bank-holidays.json. A use case for this API could potentially be a serverless function that runs annually to fetch all bank holiday dates for that year in order to better manage dispatch/delivery times for a logistics company.
Let's pause here to review the packages that we will be using:
- proxyquire: overrides dependencies
- sinon: provides us with mocks (or stubs, in this case)
- node-fetch: fetches our data
Note: The above packages work with any testing framework, but I'll be using Mocha and Chai in this particular example.
Create files
Let's start by creating our getBankHolidays
function:
// index.js
const fetch = require("node-fetch");
const BANK_HOLIDAY_URL = "https://www.gov.uk/bank-holidays.json";
const getBankHolidays = async () => {
try {
const bankHolidaysResponse = await fetch(BANK_HOLIDAY_URL);
return bankHolidaysResponse;
} catch (err) {
throw err;
}
};
module.exports = getBankHolidays;
And then the test file:
// test.js
const mocha = require("mocha");
const { expect } = require("chai");
const getBankHolidays = require("./index.js");
mocha.describe("getBankHolidays", () => {
it("should get UK Bank Holidays", async () => {
const bankHolidays = await getBankHolidays();
expect(bankHolidays).to.contain.keys([
"england-and-wales",
"northern-ireland",
"scotland"
]);
});
});
Start testing
If we run the test suite, it should pass. The call to the URL will have succeeded and the response returned will contain the expected keys.
But what if, for example, our app also relies on data from a Weather API?
We would also need to write a test for this new logic and make yet another call to a 3rd party service. Many integrations and API calls later, your testing suite could include several unit tests making various calls to live APIs. You may start to notice your CI/CD pipelines taking longer to complete or a few bugs may creep into your tests if, for example, a particular endpoint starts returning a 500 status. Or maybe the external API does not even offer a test/sandbox environment for you to use.
Clearly, there are many reasons why you shouldn't make API calls in your tests, so a great way to avoid making them in the first place is to mock the calls themselves and, if necessary, mock the response payload. Having control over both the call and response provides you with some key benefits:
- Speeds up testing
- Easier to identify problems
- More secure (due to not sharing production API credentials with CI environment)
Let me show you what I mean by coming back to our example (now with our mock request and response defined):
// mock.json
{
"england-and-wales": {
"division": "england-and-wales",
"events": [
{
"title": "New Year’s Day",
"date": "2021-01-01",
"notes": "",
"bunting": true
}
]
},
"northern-ireland": {
"division": "northern-ireland",
"events": [
{
"title": "New Year’s Day",
"date": "2021-01-01",
"notes": "",
"bunting": true
}
]
},
"scotland": {
"division": "scotland",
"events": [
{
"title": "New Year’s Day",
"date": "2021-01-01",
"notes": "",
"bunting": true
}
]
}
}
// test.js
const mocha = require("mocha");
const { expect } = require("chai");
const proxyquire = require("proxyquire");
const sinon = require("sinon");
const MOCK_BANK_HOLIDAYS = require("./mock.json");
mocha.describe("getBankHolidays", () => {
const fetchStub = sinon.stub();
fetchStub.resolves(MOCK_BANK_HOLIDAYS);
const getBankHolidays = proxyquire(
"~/Projects/exampleTest/index.js",
{
"node-fetch": fetchStub
}
);
it("should get UK Bank Holidays", async () => {
await getBankHolidays();
expect(fetchStub).to.have.been.calledOnceWithExactly(
"https://www.gov.uk/bank-holidays.json"
);
});
});
So, what exactly is happening here? Basically, proxyquire is now intercepting that API call and returning the mock data as specified in our sinon stub. In other words, from proxyquire's perspective: "If I see the getBankHolidays
module, I will replace the node-fetch
dependency with the provided stub". This means that we avoid making the external API call and can slightly change our expectations too. Instead of testing the response (which is now mocked), we can put an expectation against the request payload being sent. If this is valid, then we can safely assume that the API is correctly set up and will return the correct response.
Note: make sure your mock data replicates that which is returned by the external API in order to correctly test the outcomes and provide a valid comparison.
Conclusion
That was pretty fun, right? Nothing too tedious or complicated, I hope!
So, when testing logic that involves communicating with an external API, try using these packages to mock your calls. Having more control over this area will speed up your tests and allow you to quickly identify and resolve any issues.
For more examples, head over to proxyquire and sinon and check out their excellent docs. And for a different solution, you can also look into nock which simplifies further what we've talked about today and provides some additional capabilities.
Thanks for reading!
Top comments (1)
Can we also mock mongoose this way ?
How would mocking mongoose's model.aggregate method look like ?