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

Create React App doesn't properly mock modules from __mocks__ directory

我在 __mocks__ 目录中有一个 Jest 和 mocks 的工作示例:

使用简单的 Jest 设置

// package.json
{
  "name": "a",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
  ...
  "devDependencies": {
    "jest": "^26.6.3"
  },
  "dependencies": {
    "@octokit/rest": "^18.0.12"
  }
}

然后 /index.js :

const { Octokit } = require("@octokit/rest");

const octokit = new Octokit();

module.exports.foo = function() {
  return octokit.repos.listForOrg({ org: "octokit", type: "public" })
}

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

const { foo } = require("./index.js");

test("foo should be true", async () => {
    expect(await foo()).toEqual([1,2]);
});

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

module.exports.Octokit = jest.fn().mockImplementation( () => ({
    repos: {
        listForOrg: jest.fn().mockResolvedValue([1,2])
    }
}) );

这很好用并且测试通过了。

使用 Create React 应用程序

然而,用 Create React App 做同样的事情似乎给我一个奇怪的结果:

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

然后/src/foo.js:

import { Octokit } from "@octokit/rest";

const octokit = new Octokit();

module.exports.foo = function() {
  return octokit.repos.listForOrg({ org: "octokit", type: "public" })
}

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

const { foo} = require("./foo.js");

test("foo should be true", async () => {
    expect(await foo()).toEqual([1,2]);
});

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

export const Octokit = jest.fn().mockImplementation( () => ({
    repos: {
        listForOrg: jest.fn().mockResolvedValue([1,2])
    }
}) );

这使得测试失败:

 FAIL  src/foo.test.js
  ✕ foo should be true (2 ms)

  ● foo should be true

    expect(received).toEqual(expected) // deep equality

    Expected: [1, 2]
    Received: undefined

      2 |
      3 | test("foo should be true", async () => {
    > 4 |     expect(await foo()).toEqual([1,2]);
        |                         ^
      5 | });
      6 |
      7 |

      at Object.<anonymous> (src/foo.test.js:4:25)

阅读了大量内容后,我似乎无法让 __mocks__ 在 Create React App 中工作。有什么问题吗?

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


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

export const mockListForOrg = jest.fn();

export const Octokit = jest.fn().mockImplementation(() => ({
    repos: {
        listForOrg: mockListForOrg,
    },
}));

然后你在测试中配置你想要的值,之后 Jest 会重置它:

import { mockListForOrg } from "@octokit/rest";

import { foo } from "./foo";

test("foo should be true", async () => {
    mockListForOrg.mockResolvedValueOnce([1, 2]);

    expect(await foo()).toEqual([1, 2]);
});

另一种选择是将以下内容添加到您的 package.json 中以覆盖该配置,根据 this issue:

{
  ...
  "jest": {
    "resetMocks": false
  }
}

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


请注意,您通常不应该模拟您不拥有的东西 - 如果 @octokit/rest 的接口发生变化,您的测试将继续通过,但 您的代码将无法工作。为避免此问题,我会推荐其中一个或两个:

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

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

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

import { rest } from "msw";
import { setupServer } from "msw/node";

import { foo }  from "./foo";

const server = setupServer(rest.get("https://api.github.com/orgs/octokit/repos", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json([1, 2]));
}));

beforeAll(() => server.listen());

afterAll(() => server.close());

test("foo should be true", async () => {
    expect(await foo()).toEqual([1, 2]);
});

表明当前关于 octokit.repos.listForOrg 会 return 的假设是不准确的,因为这个测试失败了:

  ● foo should be true

    expect(received).toEqual(expected) // deep equality

    Expected: [1, 2]
    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"}

      13 | 
      14 | test("foo should be true", async () => {
    > 15 |     expect(await foo()).toEqual([1, 2]);
         |                         ^
      16 | });
      17 | 

      at Object.<anonymous> (src/foo.test.js:15:25)

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

export async function foo() {
  const { data } = await octokit.repos.listForOrg({ org: "octokit", type: "public" });
  return data;
}

或:

export function foo() {
  return octokit.repos.listForOrg({ org: "octokit", type: "public" }).then(({ data }) => data);
}