Create React App 无法正确模拟 __mocks__ 目录中的模块

dri*_*hev 7 mocking jestjs create-react-app

我有一个 Jest 的工作示例和来自的模拟__mocks__有效目录的模拟:

\n

通过简单的 Jest 设置

\n
// package.json\n{\n  "name": "a",\n  "version": "1.0.0",\n  "main": "index.js",\n  "scripts": {\n    "test": "jest"\n  },\n  ...\n  "devDependencies": {\n    "jest": "^26.6.3"\n  },\n  "dependencies": {\n    "@octokit/rest": "^18.0.12"\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

进而/index.js

\n
const { Octokit } = require("@octokit/rest");\n\nconst octokit = new Octokit();\n\nmodule.exports.foo = function() {\n  return octokit.repos.listForOrg({ org: "octokit", type: "public" })\n}\n
Run Code Online (Sandbox Code Playgroud)\n

及其测试(/index.test.js ):

\n
const { foo } = require("./index.js");\n\ntest("foo should be true", async () => {\n    expect(await foo()).toEqual([1,2]);\n});\n
Run Code Online (Sandbox Code Playgroud)\n

和模拟(/__mocks__/@octokit/rest/index.js ):

\n
module.exports.Octokit = jest.fn().mockImplementation( () => ({\n    repos: {\n        listForOrg: jest.fn().mockResolvedValue([1,2])\n    }\n}) );\n
Run Code Online (Sandbox Code Playgroud)\n

这工作得很好并且测试通过了。

\n

使用创建 React 应用程序

\n

然而,对 Create React App 进行同样的操作似乎给了我一个奇怪的结果:

\n
// package.json\n{\n  "name": "b",\n  "version": "0.1.0",\n  "dependencies": {\n    "@octokit/rest": "^18.0.12",\n    "@testing-library/jest-dom": "^5.11.4",\n    "@testing-library/react": "^11.1.0",\n    "@testing-library/user-event": "^12.1.10",\n    "react": "^17.0.1",\n    "react-dom": "^17.0.1",\n    "react-scripts": "4.0.1",\n    "web-vitals": "^0.2.4"\n  },\n  "scripts": {\n    "start": "react-scripts start",\n    "build": "react-scripts build",\n    "test": "react-scripts test",\n    "eject": "react-scripts eject"\n  },\n  ...\n}\n
Run Code Online (Sandbox Code Playgroud)\n

进而/src/foo.js

\n
import { Octokit } from "@octokit/rest";\n\nconst octokit = new Octokit();\n\nmodule.exports.foo = function() {\n  return octokit.repos.listForOrg({ org: "octokit", type: "public" })\n}\n
Run Code Online (Sandbox Code Playgroud)\n

及其测试(/src/foo.test.js ):

\n
const { foo} = require("./foo.js");\n\ntest("foo should be true", async () => {\n    expect(await foo()).toEqual([1,2]);\n});\n
Run Code Online (Sandbox Code Playgroud)\n

和完全相同的模拟(在/src/__mocks__/@octokit/rest/index.js):

\n
export const Octokit = jest.fn().mockImplementation( () => ({\n    repos: {\n        listForOrg: jest.fn().mockResolvedValue([1,2])\n    }\n}) );\n
Run Code Online (Sandbox Code Playgroud)\n

这使得测试失败:

\n
 FAIL  src/foo.test.js\n  \xe2\x9c\x95 foo should be true (2 ms)\n\n  \xe2\x97\x8f foo should be true\n\n    expect(received).toEqual(expected) // deep equality\n\n    Expected: [1, 2]\n    Received: undefined\n\n      2 |\n      3 | test("foo should be true", async () => {\n    > 4 |     expect(await foo()).toEqual([1,2]);\n        |                         ^\n      5 | });\n      6 |\n      7 |\n\n      at Object.<anonymous> (src/foo.test.js:4:25)\n
Run Code Online (Sandbox Code Playgroud)\n

读了很多书后,我似乎无法做到__mocks__在 Create React App 中工作。有什么问题?

\n

jon*_*rpe 12

问题是 CRA 的默认 Jest 设置会自动重置模拟,从而删除mockResolvedValue您设置的模拟。

\n
\n

解决这个问题的一种方法是从模块中公开模拟函数,这也使您可以更好地控制在不同的测试中使用不同的值(例如测试错误处理)并断言调用的内容

\n
export const mockListForOrg = jest.fn();\n\nexport const Octokit = jest.fn().mockImplementation(() => ({\n    repos: {\n        listForOrg: mockListForOrg,\n    },\n}));\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在Jest 重置它之后,您可以在测试中配置所需的值:

\n
import { mockListForOrg } from "@octokit/rest";\n\nimport { foo } from "./foo";\n\ntest("foo should be true", async () => {\n    mockListForOrg.mockResolvedValueOnce([1, 2]);\n\n    expect(await foo()).toEqual([1, 2]);\n});\n
Run Code Online (Sandbox Code Playgroud)\n
\n

另一种选择是根据此问题将以下内容添加到您的文件中package.json以覆盖该配置:

\n
{\n  ...\n  "jest": {\n    "resetMocks": false\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

不过,这可能会导致测试之间保留模拟状态(收到的呼叫)的问题,因此您需要确保它们在某处被清除和/或重置。

\n
\n

请注意,尽管如此,您通常不应该模拟您不拥有的东西 - 如果@octokit/rest更改的接口您的测试将继续通过,但您的代码将无法工作。为了避免这个问题,我建议使用以下之一或两者:

\n
    \n
  • 将断言移至传输层,使用MSW等检查是否发出了正确的请求;或者
  • \n
  • 编写一个简单的外观来包装@octokit/rest,将您的代码与您不拥有的接口解耦,并模拟它;
  • \n
\n

以及更高级别(端到端)测试,以确保一切都能与真正的 GitHub API 正常工作。

\n

事实上,删除模拟并使用 MSW编写这样的测试:

\n
import { rest } from "msw";\nimport { setupServer } from "msw/node";\n\nimport { foo }  from "./foo";\n\nconst server = setupServer(rest.get("https://api.github.com/orgs/octokit/repos", (req, res, ctx) => {\n    return res(ctx.status(200), ctx.json([1, 2]));\n}));\n\nbeforeAll(() => server.listen());\n\nafterAll(() => server.close());\n\ntest("foo should be true", async () => {\n    expect(await foo()).toEqual([1, 2]);\n});\n
Run Code Online (Sandbox Code Playgroud)\n

暴露出当前关于octokit.repos.listForOrg返回内容的假设是不准确的,因为此测试失败了:

\n
  \xe2\x97\x8f foo should be true\n\n    expect(received).toEqual(expected) // deep equality\n\n    Expected: [1, 2]\n    Received: {"data": [1, 2], "headers": {"content-type": "application/json", "x-powered-by": "msw"}, "status": 200, "url": "https://api.github.com/orgs/octokit/repos?type=public"}\n\n      13 | \n      14 | test("foo should be true", async () => {\n    > 15 |     expect(await foo()).toEqual([1, 2]);\n         |                         ^\n      16 | });\n      17 | \n\n      at Object.<anonymous> (src/foo.test.js:15:25)\n
Run Code Online (Sandbox Code Playgroud)\n

您的实现实际上应该看起来更像:

\n
export async function foo() {\n  const { data } = await octokit.repos.listForOrg({ org: "octokit", type: "public" });\n  return data;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

或者:

\n
export function foo() {\n  return octokit.repos.listForOrg({ org: "octokit", type: "public" }).then(({ data }) => data);\n}\n
Run Code Online (Sandbox Code Playgroud)\n