Lucas PenzeyMoog
Feb 8 · 4 min read

I recently built a project using data from the Google Books API. I knew I wanted a comprehensive test suite, but I didn’t want to test my search function using live requests to the API for a couple of reasons.

First, the data coming back from Google wouldn’t always be the same, meaning I’d have to test against a dynamic return value. I also didn’t want to spam the API every time I ran the test suite, which could run hundreds of times since I had set it to run every time I saved changes in the code.

To get around this I created a mock of the API response and stubbed it with data structured in the same format you’d see from an actual request. I used Jest for testing as I liked that I only needed to use one framework. From the beginning of the project, I knew I wanted to separate concerns as much as possible so I decided to create separate modules using Node and Browserify to bundle everything together to run in the browser.

I’m writing this for those who may not be too familiar with Jest or mocking etc. so I’ll try and describe everything as completely as possible. First, you need to install Jest as a devDependency.

With Jest installed let’s take a look at the search.fetchBooks function I wanted to test:

biblia/js/search.jsconst axios = require("axios");
const googleBooksUrl ="https://www.googleapis.com/books/v1/volumes";
const keys = require("../config/keys");
const search = {
fetchBooks: (term, index = 0) =>
axios.get(googleBooksUrl, {
params: {
q: term,
startIndex: index,
key: keys.googleBooksApiKey
}
})
.then(response => {
return response;
})
.catch(err => err.response)

As you can see I’m using Axios to perform the GET request to the API, so this is the function that I’m aiming to mock. The request takes in a few parameters such as the URI we’re sending the request to, the search term we’re basing the search on, and an index (defaulted to zero, then used to increment the search results) and the API key. Since Axios is promise-based it returns a response that we return from the function in the .then statement.

In Jest, you write your testing file names by inserting “test” after the filename you want to test. Since we’re testing search.js we’re naming our file search.test.js. Here’s the initial setup of our test:

biblia/__tests__/search.test.jsconst search = require("../js/search");test("fetches results from google books api", () => {return search.fetchBooks().then(response => {
expect(response).toEqual();
});
});

We bring in our “search” module by requiring it at the top of the file and then set up our test. We need a few more elements to make this work, most importantly the mock implementation of our Axios GET request. In a separate folder from our tests (but on the same level hierarchically as node_modules) we create the following mock:

biblia/__mocks__/axios.jsconst axios = {
get: jest.fn(() => Promise.resolve({ data: {} }))
};
module.exports = axios;

This reads as a module called axios with a function inside called get, which points to a Jest mock function that resolves a promise with a certain piece of data. This is where we’ll pass in the dummy data that represents what a real response from the API would look like. Back in our test file we now bring in our mock and set it up:

biblia/__tests__/search.test.jsconst search = require("../js/search");
const mockAxios = require("axios")
test("fetches results from google books api", () => {
mockAxios.get.mockImplementationOnce(() =>
Promise.resolve(dummy_response_data_here)
);
return search.fetchBooks().then(response => {
expect(response).toEqual();
});
});

Note that all we have to do to import our Axios mock is to require it and, for clarity’s sake, name it explicitly as a mock. Per the Jest docs:

If the module you are mocking is a Node module (e.g.: lodash), the mock should be placed in the __mocks__ directory adjacent to node_modules (unless you configured roots to point to a folder other than the project root) and will be automatically mocked. There’s no need to explicitly call jest.mock(‘module_name’).

Now that we’ve brought in our mock we can call our GET method by using Jest’s mockImplementationOnce() method. This is where we pass in the dummy data representing the API response. Since that response is a pretty hefty object it’s a good idea to put this in another folder called __fixtures__ so that your test file isn’t hundreds of lines long.

So our mock Axios has been called and will return the dummy data from our __fixtures__ folder. Now we call the actual search function and create expect statements to see whether it’s functioning properly.

const mockAxios = require("axios");
const search = require("../js/search");
const keys = require("../config/keys");
const searchResponse = require("../__fixtures__/searchResponse");
test("fetches results from google books api", () => {
mockAxios.get.mockImplementationOnce(() =>
Promise.resolve(searchResponse.raw)
);
let term = "tolkein";
let index = 0;
return search.fetchBooks(term, index).then(response => {
expect(response).toEqual(searchResponse.raw);
});
});

There we go! We call mockAxios and resolve it with our API response object, set and pass in the parameters for search.fetchBooks and then expect that our response is equal to the dummy data we passed into our mock. We can also test further items, such as if the correct parameters are passed in and how many times our mock has been implemented:

expect(mockAxios.get).toHaveBeenCalledTimes(1);
expect(mockAxios.get).toHaveBeenCalledWith(
"https://www.googleapis.com/books/v1/volumes",
{
params: {
q: term,
startIndex: index,
key: keys.googleBooksApiKey
}
}
);

Now we can run the test suite over and over again to our heart’s content without spamming the network or receiving unstable data. This was my first time mocking an API response so it took a little while to figure out the specifics, but once I had it down it made developing the rest of my project much much easier. Testing in general was critical to the success of this project, as writing tests helped me solve issues much faster than if I relied on trial and error. Having a comprehensive test suite also makes refactoring a breeze, as you know immediately whether a change has cascading consequences to other parts of the program.

Lucas PenzeyMoog

Written by

Writer, photographer, developer, Apprentice @ https://8thlight.com/

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade