DEV Community

Cover image for The only 3 steps you need to mock an API call in Jest
Zak Laughton
Zak Laughton

Posted on • Edited on • Originally published at zaklaughton.dev

The only 3 steps you need to mock an API call in Jest

I recently found myself working in a Javascript codebase where I needed to implement new Jest tests. I knew very little at the time about writing tests, so I looked to Jest docs and existing patterns in the codebase to figure out best practices and how to do it. It was fairly straightforward, and I even found myself enjoying testing. But I could not for the life of me reliably mock an API call.

Dog with caption

The docs seemed clear, and the existing code appeared to have good patterns, but there were just so many ways to mock things. The existing tests used all sorts of mocking methods such as jest.genMockFromModule(), jest.spyOn(), and jest.mock(). Sometimes the mocks were inline, sometimes they were in variables, and sometimes they were imported and exported in magical ways from mysterious __mocks__ folders. I used these techniques interchangeably every time I got a burst of confidence in understanding, only to find myself stumbling over the different methods and their effects. I had no idea what I was doing.

The issue

The issue was that I was trying to learn how to run before I even knew how to walk. Jest has many powerful ways to mock functions and optimize those mocks, but they're all useless if you don't know how to make a simple mock in the first place. And while the Jest documentation provides a lot of great insight and techniques, I couldn't figure out where to start.

In this article, I hope to give you absolute basics to mock an API call so you can benefit from my 2020 hindsight (heh). If you're going crazy like I was because you can't figure out how to just make a simple damn mock, Start here…

(NOTE: The code below was written in Node.js, but the mocking concepts also apply to frontend Javascript and ES6 modules)

The unmocked code

We're going to be testing this getFirstAlbumTitle() function, which fetches an array of albums from an API and returns the title of the first album:



// index.js
const axios = require('axios');

async function getFirstAlbumTitle() {
  const response = await axios.get('https://jsonplaceholder.typicode.com/albums');
  return response.data[0].title;
}

module.exports = getFirstAlbumTitle;



Enter fullscreen mode Exit fullscreen mode

...and here's our initial mock-less test for this function, which verifies the function actually returns the title of the first album in the list:



// index.test.js
const getFirstAlbumTitle = require('./index');

it('returns the title of the first album', async () => {
  const title = await getFirstAlbumTitle();  // Run the function
  expect(title).toEqual('quidem molestiae enim');  // Make an assertion on the result
});


Enter fullscreen mode Exit fullscreen mode

The test above does its job, but the test actually makes a network request to an API when it runs. This opens the test up to all sorts of false negatives if the API isn't working exactly as expected (e.g. the list order changes, API is down, dev machine loses network connection, etc.). Not to mention, making these requests in a large number of tests can bring your test runs to a slow crawl.

But how can we change this? The API request is being made with axios as a part of getFirstAlbumTitle(). How in the world are we supposed to reach inside the function and change the behavior?

Mock it in 3 steps

Alright, here it is. This is the big secret that would have saved me mountains of time as I was wrestling with learning mocks. To mock an API call in a function, you just need to do these 3 steps:

1. Import the module you want to mock into your test file.
2. jest.mock() the module.
3. Use .mockResolvedValue(<mocked response>) to mock the response.

That's it!

Here's what our test looks like after doing this:



// index.test.js
const getFirstAlbumTitle = require('./index');
const axios = require('axios');

jest.mock('axios');

it('returns the title of the first album', async () => {
  axios.get.mockResolvedValue({
    data: [
      {
        userId: 1,
        id: 1,
        title: 'My First Album'
      },
      {
        userId: 1,
        id: 2,
        title: 'Album: The Sequel'
      }
    ]
  });

  const title = await getFirstAlbumTitle();
  expect(title).toEqual('My First Album');
});



Enter fullscreen mode Exit fullscreen mode

What's going on here?

Let's break this down. The most important part to understand here is the import and jest.mock():



const axios = require('axios');

jest.mock('axios');


Enter fullscreen mode Exit fullscreen mode

When you import a module into a test file, then call it in jest.mock(<module-name>), you have complete control over all functions from that module, even if they're called inside another imported function. Immediately after calling jest.mock('axios'), Jest replaces every function in the axios module with empty "mock" functions that essentially do nothing and return undefined:



const axios = require('axios');
jest.mock('axios')

// Does nothing, then returns undefined:
axios.get('https://www.google.com')

// Does nothing, then returns undefined:
axios.post('https://jsonplaceholder.typicode.com/albums', {
    id: 3,
    title: 'Album with a Vengeance'
})


Enter fullscreen mode Exit fullscreen mode

So now that you've eliminated the default behavior, you can replace it with your own...



  axios.get.mockResolvedValue({
    data: [
      {
        userId: 1,
        id: 1,
        title: 'My First Album'
      },
      {
        userId: 1,
        id: 2,
        title: 'Album: The Sequel'
      }
    ]
  });



Enter fullscreen mode Exit fullscreen mode

The mocked replacement functions that Jest inserted into axios happen to come with a whole bunch of cool superpower methods to control their behavior! The most important one here, for the purposes of a simple beginner mock, is .mockResolvedValue(). When you call this on a mocked method, anything you pass in will be the default return value when the mocked function is called for the remainder of the test. Simply put: you can make axios.get() return whatever you want! And it doesn't matter whether it's called directly in your test file or as a part of a function imported into your test – Jest will mock the function no matter where it's called!

Use this newfound power to give your functions exactly what they should expect from the API calls. Stop worrying about what the network requests return, and just focus on what YOUR code does once it gets the response!

If you want to play around with the examples, feel free to use this demo repository:

GitHub logo ZakLaughton / DEMO-simple-api-mocking-with-jest

A simple API mocking example with Jest.

Wrapping up

There you have it! This is the very basics of what you need to mock functions from another module: import the module, jest.mock() the module, then insert your own return values with .mockResolvedValue()!

I recommend starting here, using only these techniques as you start building out your first mocks for your network calls. Once you have a foundational understanding of what's going on here, you can slowly start adding the other robust mocking features included in Jest.

See also: Mocking Modules (Jest documentation).

EDIT: Also, be sure to clear your mocks between tests by running jest.resetAllMocks() after each test. This will help ensure your mocks won't interfere with future tests. (Thanks for pointing this out, @mjeffe!)


Where to go from here

Alright, you've learned the basics of mocking and successfully implemented the strategies above in several tests. You can import and mock resolved values for all your API calls like an old pro. What's next?

While the methods described above will cover most simple use cases, Jest has a lot of mocking functionality and methods to do some really powerful things. You can incrementally add some of the concepts below to super-charge your mocks:

  1. Check out the other mock function methods listed in the Jest docs: Mock Functions. You can use methods such as mockReturnedValue() to mock synchronous returns and mockResolvedValueOnce() to only return a value the first time it's called.
  2. Want to see how many times a mocked function is called, what it was called with, and what it returned? Check out the mock.calls and mock.results properties (also in the Mock Functions documentation)
  3. Do you have your own custom functions that make network requests? You can mock your own modules too after they're imported into the test file: jest.mock('./path/to/js/module/file')! Careful here though that you're only mocking what's necessary. Your tests should make sure your functions do what's expected with a given mock input, and it can be easy to end up writing tests that instead just confirm you passed in mocked data.
  4. Want a function to act as it was originally written, but still want to see how many times it was called? Check out jest.spyOn().
  5. Find yourself mocking the same function over and over in multiple tests? Give it default mock responses in __mocks__ folders using Manual Mocks!

I hope this saves others some of the wasted time and frustration I went through! If anything doesn't make sense here, please leave a comment and I'd be happy to try to answer any questions. Also, let me know if there's anything else that helped you have an "Aha!" moment while learning to mock!


_Did you find this this article useful? Feel free to subscribe to my articles below or [follow me on Twitter](https://twitter.com/ZakLaughton) for more developer tips and article announcements!_

Latest comments (63)

Collapse
 
nicolasnardi404 profile image
nicolasnardi404

wow thank you so much for this article. still super relevant in 2024 hehe
i am struggling a lot to understand mock and this article helped me a lot.
definitely will save me a lot of time. going crazy this days to try to figure out tests.
I started only a few months ago into coding and only now trying to get my head around tests - also a bit disappointed on why so many tutorials dont talk about testing your code from the beginning. omg its so important. thanks for make it simple and easy to understand.

Collapse
 
lucsan profile image
lucsan

Hi Zak, apologies for not having a problem for you, just wanted to say what an excellent piece, clear concise, well written, good examples.
If I might give a note, maybe a touch lighter on the journey, we all understand your pain.
Can I also add, I think you are giving top answers and help to those asking.

Collapse
 
pthapa1 profile image
Pratik Thapa

Just wanted to say Thank You for this wonderful piece.

Collapse
 
tylerjusfly profile image
Taiwo Momoh

Hi, Zak. this still don't make sense to me. I understand you are mocking the axios right , and passing a value to it with the mockResolvedValue. but where i got confused is calling the getFirstAlbumTitle() but its not connected in any way to the value you are mocking and it seems like you are still calling the function normally as you did without the Jest.mock. How is it now getting value from the mock function.

Collapse
 
zaklaughton profile image
Zak Laughton

Thanks for the question! This confused me too, at first, and was a big driver for writing this article. This is the key part that explains it:

When you import a module into a test file, then call it in jest.mock(), you have complete control over all functions from that module, even if they're called inside another imported function.

axios is called in getFirstAlbumTitle(). Axios is not directly called in the test file, but the test file does call getFirstAlbumTitle(), which calls axios. Even though axios is called in a different file, it's still being mocked, because you set up the mock in the test file before calling the function that calls axios.

It might be clearer to see if we define the function in the test file:

// index.test.js
const axios = require('axios');

async function getFirstAlbumTitle() {
  const response = await axios.get('https://jsonplaceholder.typicode.com/albums');
  return response.data[0].title;
}

jest.mock('axios');

it('returns the title of the first album', async () => {
  axios.get.mockResolvedValue({
    data: [
      {
        userId: 1,
        id: 1,
        title: 'My First Album'
      },
      {
        userId: 1,
        id: 2,
        title: 'Album: The Sequel'
      }
    ]
  });

  const title = await getFirstAlbumTitle();
  expect(title).toEqual('My First Album');
});
Enter fullscreen mode Exit fullscreen mode

This makes the connection clearer for the purposes of demonstration, because we can see we are importing axios, including it in getFirstAlbumTitle() function definition, then mocking it. When you import the function instead...

const getFirstAlbumTitle = require('./index');
Enter fullscreen mode Exit fullscreen mode

...axios is still being mocked, even though it's not called directly in the test file.

Collapse
 
jb0925 profile image
Jesse Brink

I just wanted to say thank you for this! This really got me unblocked, so thank you for that.

Collapse
 
zaklaughton profile image
Zak Laughton

Awesome! Glad it helped!

Collapse
 
supriya_nakerikanti_5dc8a profile image
Supriya Nakerikanti

Hi Zak, This is very helpful. I am having a bit of trouble with this.

If a method is expecting the endpoint as one of its params, then how do i mock it and test the method?

test('test callAPI method', async () => {
jest.mock('axios');
axios.get.mockResolvedValue({
body: {
category: "2",
pinNo: "A-12-345-67",
pinValidity: "Valid",
planType: "Y",
relationShip: "D - Grandparent"
},
status: 200
});

const result = await callAPI();

const expected = {
    body: {
        category: "2",
        pinNo: "A-12-345-67",
        pinValidity: "Valid",
        planType: "Y",
        relationShip: "D - Grandparent"
    },
    status: 200
}

expect(result).toEqual(expected)
Enter fullscreen mode Exit fullscreen mode

});

I tried doing this and i am receiving the following error. I am trying to see if you could help me with this. Thanks in advance !

Error -

_axios.default.get.mockResolvedValue is not a function
TypeError: _axios.default.get.mockResolvedValue is not a function
at Object. (/Users/lnakerik/Desktop/eCommerce-showroom/showroom-web/ui.showroom/apps/na-showroom/src/utils/BudgetFilterPaymentOperations/BudgetFilterPaymentOperations.test.js:419:12)
at Promise.then.completed (/Users/lnakerik/Desktop/eCommerce-showroom/showroom-web/ui.showroom/node_modules/jest-circus/build/utils.js:276:28)
at new Promise ()
at callAsyncCircusFn (/Users/lnakerik/Desktop/eCommerce-showroom/showroom-web/ui.showroom/node_modules/jest-circus/build/utils.js:216:10)
at _callCircusTest (/Users/lnakerik/Desktop/eCommerce-showroom/showroom-web/ui.showroom/node_modules/jest-circus/build/run.js:212:40)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at _runTest (/Users/lnakerik/Desktop/eCommerce-showroom/showroom-web/ui.showroom/node_modules/jest-circus/build/run.js:149:3)
at _runTestsForDescribeBlock (/Users/lnakerik/Desktop/eCommerce-showroom/showroom-web/ui.showroom/node_modules/jest-circus/build/run.js:63:9)
at run (/Users/lnakerik/Desktop/eCommerce-showroom/showroom-web/ui.showroom/node_modules/jest-circus/build/run.js:25:3)
at runAndTransformResultsToJestFormat (/Users/lnakerik/Desktop/eCommerce-showroom/showroom-web/ui.showroom/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:176:21)
at jestAdapter (/Users/lnakerik/Desktop/eCommerce-showroom/showroom-web/ui.showroom/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:109:19)
at runTestInternal (/Users/lnakerik/Desktop/eCommerce-showroom/showroom-web/ui.showroom/node_modules/jest-runner/build/runTest.js:380:16)
at runTest (/Users/lnakerik/Desktop/eCommerce-showroom/showroom-web/ui.showroom/node_modules/jest-runner/build/runTest.js:472:34)

Collapse
 
veromaia222 profile image
Veronica Maia

Have you tried supertest to make http requests?

Collapse
 
chiubaca profile image
Alex Chiu • Edited

Thanks for this, very useful. I'm having a bit of trouble with this though...
my mockResolvedResponse is being returned undefined and I have no idea why!

import axios from "axios";
import { Users } from "./api-call"

jest.mock('axios')

describe('axios tests with mocking', () => {
  test('should fetch posts', async () => {

    const fakeResp = [
      {
        "userId": 1,
        "id": 2,
        "title": "qui est esse",
        "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
      }]

    mockedAxios.get.mockRejectedValue('Network error: Something went wrong');
    mockedAxios.get.mockResolvedValue(fakeResp)

    const axiosSpy = spyOn(mockedAxios, 'get')

    const result = await Users.all()

    expect(result).toEqual(fakeResp)  //❌Fails....
    expect(axiosSpy).toHaveBeenCalledTimes(1)  //✔Passes!

  });

});
Enter fullscreen mode Exit fullscreen mode

any ideas if I'm doing something silly?

Collapse
 
zaklaughton profile image
Zak Laughton • Edited

Sure! I'm not sure exactly what the root cause is, but I've got some troubleshooting steps to start.

My observations

jest.mock() vs jest.spyOn()

Looks like here you are using jest.mock() and jest.spyOn() here on the same function. Usually, these are used interchangeably, but not together. Both functions let you inspect how the function was called. The difference between the 2 is that jest.mock() completely blows away the original function being mocked, while jest.spyOn() keeps the original implementation so the function runs as it is was written. In most cases, I find I only need jest.mock(). (This article seems to do a good job diving into the comparison a bit more Understanding Jest mocks)

Here, it looks like you're spying on your mock, which is redundant, and might have unpredictable results. I'm not sure if that's the issue here, but it's a layer of complexity I'd take out. You should be able to check on the number of calls without the spy (see my suggestion in "What I'd do" below).

Mocking resolved and rejected values

Here, you're using mockedRejectedValue() and mockResolvedValue() on the same function:

mockedAxios.get.mockRejectedValue('Network error: Something went wrong');
mockedAxios.get.mockResolvedValue(fakeResp)
Enter fullscreen mode Exit fullscreen mode

This is redundant because each one will completely overwrite the mocked implementation, so first you set it to reject (no matter what), then you set it to resolve no matter what. Since your expected output (mockResolvedValue(fakeResp)) comes second, the .mockRejectedValue('Network error: Something went wrong') has no impact here. It won't change the output, but I'd remove it just to reduce the complexity for troubleshooting.

mockRejectedValue() is typically only needed if you are explicitly testing an error state (See also: Jest docs for mockRejectedValue() and mockResolvedValue()).

What I'd do

With the notes above, I'd remove some of the redundant code, then if it's still not working, dig into how the mocked function is being called:

  1. Remove the spyOn()

    - const axiosSpy = spyOn(mockedAxios, 'get')
        [...]
    - expect(axiosSpy).toHaveBeenCalledTimes(1) 
    + expect(axios.get).toHaveBeenCalledTimes(1) 
    
  2. Remove the mockRejectedValue()

    - mockedAxios.get.mockRejectedValue('Network error: Something went wrong');
    
  3. If the issue still isn't resolved, you can dig into what axios.get is being called with and what it's returning:

    console.log("axios.get() called with>>>", axios.get.mock.calls[0]);
    console.log("axios.get() returns>>>", axios.get.mock.results[0]);
    

    This should show exactly how axios.get() is being called in Users.all() (see more details on this type of mock call inspection in the jest docs here: Mock Functions). You can also throw some console.logs in the actual Users.all() function, too, which will also output to the terminal during the test.

Here's an example of what that console.log output looks like when I add it to the sample code from this article:

code screenshot

I hope this helps!

Collapse
 
ramyac032001 profile image
c Ramya

can i except the data in the screen to bind in the front end like
expect(screen.getByText(''my first album')).toBeInTheDocument();
but i try this above line ,it should not work .give any soltuion for that?

Collapse
 
chiubaca profile image
Alex Chiu • Edited

Wooah thanks for such a detailed reply!

I forgot to mention one crucial piece of information. I'm trying to do this with TypeScript! ** plot-twist! **

This means I get errors when trying to use axios.get.mock. I think this why I started playing around with jest spies, as it a bit more of type friendly method of getting the assertion metadata out.

Thread Thread
 
zaklaughton profile image
Zak Laughton

Ah, got it! Yeah, how to type mock functions is not immediately clear. Try this:

(axios.get as jest.Mock).mockResolvedValue(fakeResp)
Enter fullscreen mode Exit fullscreen mode

That should at least pass type checking and give you the auto-complete in your editor for mock functions. This should be good enough to at least get it working. If I remember correctly though, it won't actually check the types on the resolved value, so fakeResp could be any type, even if it doesn't match the return type of Users.all(). You'll also have to add as jest.Mock everywhere you call axios.get

If you want stricter typing for this without needing to cast as jest.Mock each time, I've had a great experience with ts-jest. Looks like they've updated a lot since I used it last, so I can't give a syntax example, but you can check out their docs.

tl;dr: use (axios.get as jest.Mock) for generic mock function types, or use a tool like ts-jest for stricter types of that specific mock function.

Thread Thread
 
chiubaca profile image
Alex Chiu

Thank you so much! Was finally able to get the test passing! The trick of using (axios.get as jest.Mock) was the key to letting me debug this thoroughly.

Collapse
 
espretto profile image
espretto

In order to mock API responses, use mswjs.io/

Collapse
 
neutrinosunset profile image
Neutrino

That's great. But how do you mock an method of a class instead of a top level function that is exported directly?