How to mock Axios with Jest?

12,465

Solution 1

I would recommend an entirely different way of approaching this. Rather than trying to mock Axios, which is a relatively complicated API that you don't own, test at the network boundary using a tool like msw. This allows you to freely refactor the implementation without needing to change the tests, giving you more confidence it's still working. You could do things like:

  • Factor out repeated config to axios.create({ baseURL: "http://localhost", ... });
  • Switch to a different library for the requests (e.g. node-fetch).

Also if the Axios API changed your tests would start failing, telling you your code no longer works. With a test double, as that would still implement the previous API, you'd have passing but misleading test results.

Here's how that kind of test might look; note that Axios isn't mentioned at all, it's just an implementation detail now and we only care about the behaviour:

import { rest } from "msw";
import { setupServer } from "msw/node";

import client from "./";

const body = { hello: "world" };

const server = setupServer(
  rest.get("http://localhost", (_, res, ctx) => {
    return res(ctx.status(200), ctx.json(body))
  })
);

describe("Client", () => {
    beforeAll(() => server.listen());

    afterEach(() => server.resetHandlers());

    afterAll(() => server.close());

    it("should call the API and return a response", async () => {
        const response = await client.createRequest("http://localhost/", "GET");

        expect(response).toMatchObject({ data: body, status: 200 });
    });
});

Note I've had to use .toMatchObject because you're exposing the whole Axios response object, which contains a lot of properties. This isn't a good API for your client, because now everything using the client is consuming the Axios API; this makes you heavily coupled to it, and dilutes the benefits I mentioned above.

I'm not sure how you're planning to use it, but I'd be inclined to hide the details of the transport layer entirely - things like status codes, headers etc. are not likely relevant to the business logic in the consumer. Right now you really just have:

const createRequest = (url, method) => axios({ method, url });

at which point your consumers might as well just be using Axios directly.

Solution 2

jest.doMock(moduleName, factory, options) method will NOT automatically be hoisted to the top of the code block. This means the axios function used in the createRequest function will still be the original one.

You need to use jest.mock().

E.g.

index.js:

import axios from 'axios';

const createRequest = async (url, method) => {
  const response = await axios({
    url: url,
    method: method,
  });
  return response;
};

export default { createRequest };

index.test.js:

import axios from 'axios';
import client from './';

jest.mock('axios', () => jest.fn(() => Promise.resolve('teresa teng')));

describe('Client', () => {
  it('should call axios and return a response', async () => {
    const response = await client.createRequest('http://localhost/', 'GET');
    expect(axios).toHaveBeenCalled();
    expect(response).toEqual('teresa teng');
  });
});

unit test result:

 PASS  examples/67101502/index.test.js (11.503 s)
  Client
    ✓ should call axios and return a response (4 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |                   
 index.js |     100 |      100 |     100 |     100 |                   
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        13.62 s
Share:
12,465
doctorsherlock
Author by

doctorsherlock

Software Engineer

Updated on June 11, 2022

Comments

  • doctorsherlock
    doctorsherlock almost 2 years

    I have a function in client/index.js which is using axios to make a request

    import axios from "axios";
    
    const createRequest = async (url, method) => {
        const response = await axios({
            url: url,
            method: method
        });
        return response;
    };
    
    export default { createRequest };
    

    I want to test this function using jest, so I created client/index.test.js

    import { jest } from "@jest/globals";
    import axios from "axios";
        
    import client from "./";
    
    jest.doMock('axios', () => jest.fn(() => Promise.resolve()));
    
    describe("Client", () => {
    
        it("should call axios and return a response", async () => {
            const response = await client.createRequest('http://localhost/', 'GET');
    
            expect(axios).toHaveBeenCalled();
        })
    })
    

    But when I try to run this, the test is failing and I am getting this error

    connect ECONNREFUSED 127.0.0.1:80
    

    If I use mock instead of doMock, then I am getting this error -

    ReferenceError: /Users/project/src/client/index.test.js: The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables.
        Invalid variable access: jest
    

    package.json -

    {
        "name": "project",
        "version": "0.0.1",
        "main": "index.js",
        "author": "author",
        "license": "MIT",
        "private": false,
        "type": "module",
        "scripts": {
            "start": "node --experimental-json-modules --experimental-specifier-resolution=node ./src/index.js",
            "start:dev": "nodemon --experimental-json-modules --experimental-specifier-resolution=node ./src/index.js",
            "test": "node --experimental-vm-modules node_modules/.bin/jest",
            "test:dev": "node --experimental-vm-modules node_modules/.bin/jest --watch",
            "test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage",
            "lint": "eslint --fix .",
            "pretty": "prettier --write ."
        },
        "dependencies": {
            "axios": "^0.21.1",
            "express": "^4.17.1"
        },
        "devDependencies": {
            "babel-eslint": "^10.1.0",
            "eslint": "^7.23.0",
            "jest": "^26.6.3",
            "prettier": "^2.2.1",
            "supertest": "^6.1.3"
        },
        "jest": { "testEnvironment": "node" }
    }
    

    I am running this in node env and node version is 14.16.0, jest version is 26.6.3. Please help to identify what is wrong in this approach and how to fix it.

    • Phil
      Phil about 3 years
      Try using jest.mock instead of jest.doMock. The latter is not hoisted above import statements as per the documentation
    • Phil
      Phil about 3 years
      Remove the @jest/globals import. Can't see any reason to have it there as jest is always in scope when running tests
    • doctorsherlock
      doctorsherlock about 3 years
      @Phil if I remove @jest/globals then I am getting this ReferenceError: require is not defined.
    • Phil
      Phil about 3 years
      See this post for answers
    • doctorsherlock
      doctorsherlock about 3 years
      @Phil I have added package.json, maybe that will help.
  • doctorsherlock
    doctorsherlock about 3 years
    Thanks for the response, like in your answer I changed to jest.mock() and removed import of @jest/globals. Then i get this error - ReferenceError: require is not defined.
  • Estus Flask
    Estus Flask about 3 years
    Actually, mocking Axios makes perfect sense, since it provides enough abstraction over XHR/Node http via adapters and interceptors and was designed with these concerns in mind. In case complex API is an issue, there's official Moxios library, as well as several independent ones. MSW makes more sense in integration/e2e scenarios that can't or shouldn't be focused on specific implementation. Also, network transport isn't something that a user owns or fully controls either, Nock and MSW for Node do much more black magic than Axios mocking would ever need.
  • jonrsharpe
    jonrsharpe about 3 years
    @EstusFlask that doesn't really address what I said. It's a well architected abstraction, but still not one you own. The network layer is at least an existing seam, if another is needed introducing a facade you do own and replacing that with a test double minimises coupling between your code and tests and the third party interface.
  • Estus Flask
    Estus Flask about 3 years
    The problem is that test double operates on API that wasn't designed to be mocked this way, i.e. patches Node http lib, likely internal parts of it. I don't remember if this caused problems for me with Nock but I'm sure there are known issues. It's a legit tool if it serves the purpose but I wouldn't think of it as of a superior solution. I don't see much problems with coupling, as long as it's unit or integration test that has access to Axios instances, as required by Axios testing libs. Making every test as tolerable to all possible refactorings as it can be is a bit far-fetched approach.