Introduction
Following on from the first part of this tutorial series: Building a Jest Clone, we’re now going to build out our own mocks.
What is mocking in tests?
Mocking is a really useful testing technique which allows you to replace dependencies with objects which simulate their behaviour. In other words, you replace your real function or module with a fake version, or ‘mock’, in order to test it.
But why would we do that?
Well, it can be really useful for testing things where you maybe make an API call, or do something like charge a credit card.
You don’t want to rely on whether the API is up or not in a unit test, and API calls take time. As for payment testing, well maybe you don’t want to run a real transaction every time you run a test! Instead, you drop in code which simulates a normal response so you can test your code, not an external dependency.
Mocks to the rescue
So as we saw in our initial tutorial, we created a very basic test runner and added some assertions. Cool! So we can check if something matches what we expect, whether it’s truthy, or falsy, or null, etc.
But now we want to test against an API call we run at work. It looks something like this:
import axios from "axios"
const getData = async (url: string) => {
try {
const response = await axios.get(url)
return response.data
} catch (error) {
return `Error: ${error}`
}
}
This is obviously a very simple example and not necessarily something we’d push to production, but for our purposes it works fine. Our imaginary API returns a response like this:
[
{
"id": "2baf70d1-42bb-4437-b551-e5fed5a87abe",
"name": "Test User",
"email": "test@example.com"
},
{
"id": "5daf90d1-42bb-4437-b551-e5fed5a87zyj",
"name": "Another Person",
"email": "a.person@example.com"
}
{
"id": "5daf90d1-42bb-6637-d521-e5fed5a87qqq",
"name": "Some Name",
"email": "name@example.com"
}
]
A fairly straightforward response! So we might write a test like this:
test("it returns an API response", async () => {
const result = await getData('https://imaginaryapi.com/users')
const expected = [
{
"id": "2baf70d1-42bb-4437-b551-e5fed5a87abe",
"name": "Test User",
"email": "test@example.com"
},
{
"id": "5daf90d1-42bb-4437-b551-e5fed5a87zyj",
"name": "Another Person",
"email": "a.person@example.com"
}
{
"id": "5daf90d1-42bb-6637-d521-e5fed5a87qqq",
"name": "Some Name",
"email": "name@example.com"
}
]
expect(result).toBe(expected)
This works in theory, but what if the API was down? What if we were charged per request? Instead, we want to mock this. We could just hardcode an expected response, but that’s not very DRY (Don’t Repeat Yourself). Instead, we can extract this out to its own utility.
Build your own mocks
To build our own mock, we want to create a function, which accepts another function as an argument. We will then create a mock function will accept any arguments, which we will then forward on to the function we took as an argument. Then we return our mocked function.
So I feel like I wrote function
a tonne here, so let’s take a look at some code:
/**
*
* @param functionToBeMocked Function
* @returns Function
*
* @description
* Allows us to mock a function by returning a new function
* specified by the user in place of existing functionality.
*
*/
const fn = (functionToBeMocked: Function = () => {}) => {
const mockFn = (...args: any) => {
mockFn.mock.calls.push(args)
return functionToBeMocked(...args)
}
mockFn.mock = { calls: [] }
return mockFn
}
So to break it down, we:
Accept a function which we want to mock.
We create a mock function which accepts arguments.
We then add a
mock
property to our function and acalls
array to which we push the arguments we called the mocked function with.Finally we return the mocked function.
Using our new mock
So now we have a shiny new mock implementation, how do we use it? Well, it’s pretty simple! Let’s update our initial test:
test("it returns an API response", async () => {
const data = [{"id": 1, "name": "Test User", "email": "hello@example.com"}]
let getData = fn((url: string) => data)
const result = await getData('https://imaginaryapi.com/users')
expect(result).toBe(data)
As you can see, we created our own data which we expect to be returned, and effectively hijack the getData
function to return our own implementation.
Then we call our newly mocked function and the result matches the expected data!
We could also call:
console.log(getData.mock.calls)
and we would receive:
'https://imaginaryapi.com/users'
which was the argument we passed to our mocked function.
Conclusion
As you can see, mocking is a simple yet powerful technique which allows you to emulate external dependencies, regardless of whether they’re packages or external API calls. It allows you to make your unit tests less brittle, and quicker.
By creating your own mocks, you can understand what jest, or similar libraries are doing beyond running jest.fn()
and gain a better understanding of the tooling that you use every day.
Thanks again to Kent C Dodd’s Testing JavaScript course which is invaluable for learning this kind of thing.