I donβt know about you but I love Full stack development. It has a variety of problems to solve and is the way forward if understanding the big picture of how a system works is your cup of tea. However one thing that I often find quite frustrating in this picture is mocking code in unit tests. Before we go to straight to the point of this blog let's talk about the idea behind mocking.
Mocking is making code assertions that will trigger specific system behaviours in unit tests. In simple words, it is a way of forcing the code to return the output that we want in order for tests to pass. Why exactly we need to do that? That can be for a number of reasons like skipping certain parts of the code and focusing on testing the pieces that we want. A good example is when we want to go through code that is out of our control. You know... Stuff like api calls, DB query results or anything else that is out of the unit testβs reach.
Mocking is a technique proven to be quite powerful for isolating tests. That way they do not rely on other services being available. One of the benefits of mocking services is that it can prevent noise from happening on a unit test level.
Like many things in the development world a concept may sound simple in theory but implementing it can be quite challenging. Before we go deep into code let's see how mocking a function looks like. I hope you like dogs. If you do check this awesome and free API for getting photos of your favourite doggies. πΆ
// randomDog.js
export const randomDog = () => {
const breeds = ["samoyed", "husky", "chow", "shiba", "pembroke"]
return breeds[Math.floor(Math.random() * breeds.length)]
}
It is common to have no absolute control of what functions return. If we ever find ourselves in such a situation, mocking that function can be our strong ally. Here is a simple example of mocking the randomDog
function we just saw.
jest.mock("./randomDog", () => ({
randomDog: () => "samoyed",
}))
Let me explain what is going on here. We are:
- passing the path to the file as the first argument
- it gets reset
- define the output we want it to return as the second argument
Now we are forcing the function randomDog
to return a different value comparing to what it normally would. That gives us the freedom to take our tests to the next level. How about more complex cases? Not a problem. We can follow the same logic but instead of a simple string we can return deeply nested objects. Let's see example of some async code we can mock.
// getDog.js
import { randomDog } from "./randomDog"
import { getDogAPI } from "./getDogAPI"
export const getDog = async () => {
const random = randomDog()
const { message } = await getDogAPI(random)
return message
}
The getDogAPI
function calls API that returns pictures of the dog breed we passed as input. Who said testing could not be fun?
import { getDog } from "./getDog"
import { getDogAPI } from "./getDogAPI"
jest.mock("./getDogAPI", () => ({
getDogAPI: () =>
Promise.resolve({
message: "some/url/with/samoyed/photos.jpg",
status: "success",
}),
}))
test("return a photo of a samoyed", async () => {
const doggieResult = await getDog()
expect(doggieResult.includes("samoyed")).toEqual(true)
})
Oops here things get a bit more complicated isn't it? Please take some time to digest it. We already know that getDogAPI
is an async function. Returning a promise allows our code to look at this mock as something running asynchronously. We then resolve this promise to return the result we want. Similarly we can test with Promise.reject for error handling.
It is important to notice that we import the getDogAPI
but apart from mocking it, we do not make any use of it inside this file. In this case we only want to mock a function that we do not use directly inside our test but one of the functions we test does. Mocking can be confusing because it uses a slight different flow of code comparing to what we are used to. Another downside is that when we mock something, we mock the entire file that uses it. Anything that is exported out of it is going to be undefined
until we instruct our mock function to give it a value. That requires us to abstract our code even further. But to be honest this is not necessarily a bad thing as it requires us to write more structured code.
Knowing how to mock async code means that we now we do not really rely on any APIs in order to write our unit tests. However in certain cases we may need to mock modules. Let's take even deeper and mock the axios module itself.
// getDogAPI.js
import axios from "axios"
export const getDogAPI = async (breed: string): Promise<any> => {
return await axios.get(`https://dog.ceo/api/breed/${breed}/images/random`)
}
Mocking modules sounds a bit scary but if we have reached that far into mocking then it is quite straightforward.
import { getDog } from "./getDog"
import mockAxios from "axios" // 1 import module
jest.mock("axios") // 2 mock / reset it
test("now I want to see a photo of a husky", async () => {
const mockResult = {
message: "some/url/with/husky/photos.jpg",
status: "success",
}
mockAxios.get.mockResolvedValue(mockResult) // 3 resolve its value
// or with Typescript
// (mockAxios.get as jest.Mock).mockResolvedValue(mockResult)
const anotherDoggie = await getDog()
expect(anotherDoggie.includes("husky")).toEqual(true)
})
The approach here is very similar to our previous examples but we now split the logic into different stages. That logic goes as follows:
- We import the module we want to mock
- We mock it so that its original functionality is reset
- We resolve its value to one that makes sense for our test scenario
The whole magic happens with mockResolveValue
which tells the code when the getDogAPI
gets called, then return the mockResult
we told you to return. Jest has several mock functions depending on your testing needs. A full list can be found here.
I have found testing to be a crucial part of writing high quality software. The thing I like the most about testing is that (especially when mocking) it helps us have a deeper understanding of our code flow. That is really important because it helps us get a clearer picture of the mechanics behind how the code we write actually works. That is all for now.
Some useful links:
Top comments (0)