如何用 Jest 模拟 Axios?

doc*_*ock 13 javascript unit-testing node.js jestjs axios

client/index.js我有一个使用 axios 发出请求的函数

import axios from "axios";

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

export default { createRequest };
Run Code Online (Sandbox Code Playgroud)

我想使用 测试这个函数jest,所以我创建了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();
    })
})
Run Code Online (Sandbox Code Playgroud)

但是当我尝试运行它时,测试失败并且出现此错误

connect ECONNREFUSED 127.0.0.1:80
Run Code Online (Sandbox Code Playgroud)

如果我使用模拟而不是 doMock,那么我会收到此错误 -

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
Run Code Online (Sandbox Code Playgroud)

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" }
}
Run Code Online (Sandbox Code Playgroud)

我在节点环境中运行它,节点版本是14.16.0,笑话版本是26.6.3。请帮助确定这种方法有什么问题以及如何修复它。

jon*_*rpe 16

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 });
    });
});
Run Code Online (Sandbox Code Playgroud)

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 });
Run Code Online (Sandbox Code Playgroud)

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

  • 实际上,模拟 Axios 是非常有意义的,因为它通过适配器和拦截器对 XHR/Node http 提供了足够的抽象,并且在设计时就考虑到了这些问题。如果复杂的 API 是一个问题,有官方的 Moxios 库,以及几个独立的库。MSW 在不能或不应该专注于特定实施的集成/e2e 场景中更有意义。此外,网络传输也不是用户拥有或完全控制的东西,Nock 和 MSW for Node 所做的黑魔法比 Axios 模拟所需要的要多得多。 (9认同)
  • 问题是测试替身在 API 上运行,而 API 并不是设计来以这种方式模拟的,即修补 Node `http` lib,可能是它的内部部分。我不记得这是否给我的 Nock 带来了问题,但我确信存在已知问题。如果它能达到目的,那么它就是一个合法的工具,但我不认为它是一个更好的解决方案。我没有看到太多耦合问题,只要它是可以访问 Axios 实例的单元或集成测试,正如 Axios 测试库所要求的那样。使每个测试都能够容忍所有可能的重构是有点牵强的方法。 (3认同)

sli*_*wp2 8

jest.doMock(moduleName, factory, options)方法不会自动提升到代码块的顶部。这意味着axios该函数中使用的函数createRequest仍将是原始函数。

\n

你需要使用jest.mock().

\n

例如

\n

index.js:

\n
import axios from \'axios\';\n\nconst createRequest = async (url, method) => {\n  const response = await axios({\n    url: url,\n    method: method,\n  });\n  return response;\n};\n\nexport default { createRequest };\n
Run Code Online (Sandbox Code Playgroud)\n

index.test.js:

\n
import axios from \'axios\';\nimport client from \'./\';\n\njest.mock(\'axios\', () => jest.fn(() => Promise.resolve(\'teresa teng\')));\n\ndescribe(\'Client\', () => {\n  it(\'should call axios and return a response\', async () => {\n    const response = await client.createRequest(\'http://localhost/\', \'GET\');\n    expect(axios).toHaveBeenCalled();\n    expect(response).toEqual(\'teresa teng\');\n  });\n});\n
Run Code Online (Sandbox Code Playgroud)\n

单元测试结果:

\n
 PASS  examples/67101502/index.test.js (11.503 s)\n  Client\n    \xe2\x9c\x93 should call axios and return a response (4 ms)\n\n----------|---------|----------|---------|---------|-------------------\nFile      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s \n----------|---------|----------|---------|---------|-------------------\nAll files |     100 |      100 |     100 |     100 |                   \n index.js |     100 |      100 |     100 |     100 |                   \n----------|---------|----------|---------|---------|-------------------\nTest Suites: 1 passed, 1 total\nTests:       1 passed, 1 total\nSnapshots:   0 total\nTime:        13.62 s\n
Run Code Online (Sandbox Code Playgroud)\n