How to mock Axios with Jest?

Viewed 18108

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.

3 Answers

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.

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

If you can, try to restructure your code to use the Dependency Injection pattern.

Example without DI:

/**
 * Example without dependency injection.
 */
class MyService {
  async doThing() {
    const axios = require("axios");
    return (await axios.request({
      method: "GET",
      url: "http://example.com",
      data: "",
      headers: {},
    })).data
  }
}

const s = new MyService();

s.doThing();

Example with DI:

/**
 * Example with dependency injection.
 */
class MyService {
  /**
   * @param request
   *   An async function that takes method, url, body and headers as arguments and returns a promise that resolves to
   *   the deserialized response body.
   */
  constructor(request) {
    this.request = request;
  }

  async doThing() {
    return this.request("GET", "http://example.com", "", {});
  }
}

const s = new MyService(async (method, url, body, headers) => {
  const axios = require("axios");
  return (await axios.request({
    method,
    url,
    data: body,
    headers,
  })).data;
});

s.doThing();

This can be improved even further using TypeScript:

/**
 * An async function that takes method, url, body and headers as arguments and returns a promise that resolves to
 *   the deserialized response body.
 */
export type RequestFunction = (
    method: "GET" | "POST",
    url: string,
    body: string,
    headers: Record<string, string>
) => Promise<any>;

/**
 * Example with dependency injection.
 */
class MyService {
    public readonly request: RequestFunction;

    constructor(request: RequestFunction) {
        this.request = request;
    }

    async doThing() {
        return this.request("GET", "http://example.com", "", {});
    }
}

const s = new MyService(async (method, url, body, headers) => {
    const axios = require("axios");
    return (await axios.request({
        method,
        url,
        data: body,
        headers,
    })).data;
});

s.doThing();

This had many benefits:

  • Code is more modular.
  • Mocking becomes VERY easy, regardless of your test framework.
  • You don't force your favorite request library on your users.

To test:

describe("MyService", () => {
  it("does a thing", async () => {
    const s = new MyService(async () => ({ foo: "bar" }));
    expect(await s.doThing()).toEqual({ foo: "bar" });
  })
})
Related