How to mock Axios with Jest?
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
Comments
-
doctorsherlock almost 2 years
I have a function in
client/index.js
which is using axios to make a requestimport 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 createdclient/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 is26.6.3
. Please help to identify what is wrong in this approach and how to fix it.-
Phil about 3 yearsTry using
jest.mock
instead ofjest.doMock
. The latter is not hoisted aboveimport
statements as per the documentation -
Phil about 3 yearsRemove the
@jest/globals
import. Can't see any reason to have it there asjest
is always in scope when running tests -
doctorsherlock about 3 years@Phil if I remove
@jest/globals
then I am getting thisReferenceError: require is not defined
. -
Phil about 3 yearsSee this post for answers
-
doctorsherlock about 3 years@Phil I have added package.json, maybe that will help.
-
-
doctorsherlock about 3 yearsThanks 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 about 3 yearsActually, 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 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 about 3 yearsThe 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.