Mocking asynchronous functions with Jest
An overview of how to unit test asynchronous functions and AJAX calls with Jest
Published: 29 December 2019
Consider a simple function which return a promise like so:
// index.js
function giveMePromise() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("I am done"), 2000)
})
}
NOTE: We always need to return the promise in our original function otherwise Jest will get an undefined value when trying to make assertions.
We can use one of the following ways to test our function:
//index.test.js
import giveMePromise from "./index.js"
// 1. good old then block
describe("giveMePromise", () => {
it("resolves with the correct value", done => {
giveMePromise().then(res => expect(res).toBe("I am done"))
})
})
//2. or the cutting edge Async/Await
describe("giveMePromise", () => {
it("resolves with the correct value", async () => {
let result = await giveMePromise()
expect(result).toBe("I am done")
})
})
In the above code, we first passed a done argument to the it block (to tell Jest to wait until the promise is resolved).
Then, we invoked our asynchrounous function as usual and once we had made our assertion, we called done which is a function to tell Jest we are done.
We could just as easily use Async/Await as shown above. Notice the async keyword in the callback instead of done as an argument.
This approach obviously has the downside that we need to wait for 2sec or however many seconds it will take to resolve the request. This will slow down our suite of tests if we have many tests to run.
//index.test.js
//NOT importing giveMePromise from index.js
//instead creating a mock
const giveMePromise = jest.fn(() => Promise.resolve("I am done without delay"))
describe("giveMePromise", () => {
it("resolves with the correct value", async () => {
let result = await giveMePromise()
expect(res).toBe("I am done without delay")
})
})
This second approach involves creating a mock of the original function giveMePromise. The in-built utility function jest.fn is used for mocking, it takes a callback inside which we can execute any code we want.
Basically a mock is a fake function which is invoked eactly like the original but its implementation is written in a way that it gives us the desired value without any delays or without actually calling an API endpoint as in the case of AJAX calls (more on that below).
Notice how we didn't import the original function into our test file since it would have caused a namespace conflict.
Lets try hitting a real API and use axios library as http client for making requests.
//index.js
import axios from "axios"
function getSingleTodoTitle() {
let url = "https://jsonplaceholder.typicode.com/todos/1"
return axios
.get(url)
.then(res => res.data.title.toUpperCase())
.catch(err => err.response) // contains {status:400, statusText:"Some text"}
}
export default getSingleTodoTitle
Here we are reaching out to a fake API and getting the title of the first todo and then making it all uppercase.
The promise from Axios returns the data in the following format:
res: {
data: {
//our data lives here as an object or as an array of objects
}
}
//in getSingleTodoTitle res was
res = {
data: {
userId: 1,
id: 1,
title: "delectus aut autem",
completed: false,
},
}
// OR in case of the error
err = {
response: {
//stuff about error lives here as sent by the API
},
}
Its important to realise what to mock. The most time consuming part of the function is the axios call, so we are only going to mock that part and let the rest work as usual.
//index.test.js
import axios from "axios"
import getSingleTodoTitle from "./../index"
test("gets the uppercased title of the todo", async () => {
let fakeResponse = {
data: {
userId: 1,
id: 1,
title: "Make me Uppercase",
completed: false,
},
}
axios.get = jest
.fn()
.mockImplementationOnce(() => Promise.resolve(fakeResponse))
let todoTitle = await getSingleTodoTitle() //now jest will use the mock instead of actual API call
expect(todoTitle).toBe("MAKE ME UPPERCASE")
})
We mocked only the GET request of Axios and resolved our promise in the same structure as we would get from the API endpoint (without ever actually hitting the endpoint).
Jest picks up that we have created a mock for axios's GET request and uses it accordingly. Make sure to implement the mock inside the test block or else it will override other mocks and wreak havoc basically.
We could have used just jest.fn instead of jest.fn.mockImplementationOnce but this may cause issues of unresolved promises if we were to use another mock in the same file for axios's GET.
For example, lets say now we want to test that the error is handled correctly during the GET request. We can make it work like so:
test("gives an error and handles it correctly", async () => {
let fakeError = {
response: {
status: 404,
statusText: "Not Found",
},
}
axios.get = jest.fn().mockImplementationOnce(() => Promise.reject(fakeError)) //not going to mess with other mocks
let errorObj = await getSingleTodoTitle()
expect(errorObj).toBe(fakeError.response)
})
We follow the same procedure as above:
But writing mock implementations for all different GET requests will make our test files long, unreadable and confusing. To avoid this, we can mock the whole modules like axios. A module is just a fancy name for an object or a function which lives in a different file. In our case, index.js file exports getSingleTodoTitle, so that is a module for testing purposes.
Let's create **mocks** folder at the same level as index.js and add axios.js file in this folder. The name of the file must match the name of the module to be mocked which is why we named it axios.js.
//__mocks__/axios.js
let fakeData = {
data: {
userId: 1,
id: 1,
title: "Make me Uppercase",
completed: false,
},
}
const axios = {
get: function(url) {
if (url === "https://jsonplaceholder.typicode.com/todos/1") {
return Promise.resolve(fakeData)
}
},
}
export default axios
Now lets use this inside our main test file:
//index.test.js
const axios = require("axios") //just as normal
const getSingleTodoTitle = require("./../index")
jest.mock("axios") //this tells Jest to look for mocks folder and use axios from there
test("gets the uppercased title of the todo", async () => {
let todoTitle = await getSingleTodoTitle()
expect(todoTitle).toBe("MAKE ME UPPERCASE")
})
Our code is now shrunk to just few lines and it looks more structured.
But what happens to our error scenario, how do we manage that?
We can use the magic of mockImplementationOnce as we did above in the error case to tell Jest to use our own function for only one time. This way, jest ignores the axios file in the mocks folder and uses whatever function we specify.
By the way, now we can keep adding more calls to our mocked axios file without ever needing to add any code in our test file. For instance lets add another GET and a POST call:
//__mocks__/axios.js
let fakeData = {
data: {
userId: 1,
id: 1,
title: "Make me Uppercase",
completed: false,
},
}
const axios = {
get: function(url) {
if (url === "https://jsonplaceholder.typicode.com/todos/1") {
return Promise.resolve(fakeData)
} else if (url === "https://jsonplaceholder.typicode.com/posts") {
Promise.resolve({ data: [{ title: "First post", id: "wew2323" }] })
}
},
post: function(url) {
return Promise.resolve({ data: { success: "OK" } })
},
}
export default axios
Github Repo for this post's sample code