Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock Axios with Jest?

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.

like image 737
doctorsherlock Avatar asked Apr 15 '21 02:04

doctorsherlock


People also ask

How do I test Axios post in Jest?

To test axios in Jest, we can mock the axios dependency. import * as axios from "axios"; jest. mock("axios"); // ... test("good response", () => { axios.

How do you mock data in Jest?

In Jest, Node. js modules are automatically mocked in your tests when you place the mock files in a __mocks__ folder that's next to the node_modules folder. For example, if you a file called __mock__/fs. js , then every time the fs module is called in your test, Jest will automatically use the mocks.


2 Answers

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
like image 152
slideshowp2 Avatar answered Oct 09 '22 07:10

slideshowp2


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.

like image 9
jonrsharpe Avatar answered Oct 09 '22 08:10

jonrsharpe