Create React App无法正确模拟__mocks__目录中的模块 [英] Create React App doesn't properly mock modules from __mocks__ directory

查看:48
本文介绍了Create React App无法正确模拟__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 App

然而,使用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]);
});
 


另一个选择是,根据 { ... "jest": { "resetMocks": false } }

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


请注意,尽管您通常不应该嘲笑自己不拥有的东西-如果@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将返回什么的当前假设是不准确的,因为此测试失败:

   ● 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);
}
 

I have a working example with Jest and mocks from __mocks__ directory that works :

With simple Jest setup

// 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"
  }
}

And then /index.js :

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

const octokit = new Octokit();

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

with its test (/index.test.js):

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

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

and the mock (/__mocks__/@octokit/rest/index.js):

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

This works quite well and tests pass.

With Create React App

However doing the same with Create React App seems to be giving me a weird result:

// 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"
  },
  ...
}

And then /src/foo.js:

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

const octokit = new Octokit();

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

with its test (/src/foo.test.js):

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

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

and the very same mock (under /src/__mocks__/@octokit/rest/index.js):

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

This makes the test fail:

 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)

After reading a lot it seems that I can't make __mocks__ work inside Create React App. What's the problem?

解决方案

The problem is that CRA's default Jest setup automatically resets the mocks, which removes the mockResolvedValue you set.


One way to solve this, which also gives you more control to have different values in different tests (e.g. to test error handling) and assert on what it was called with, is to expose the mock function from the module too:

export const mockListForOrg = jest.fn();

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

Then you configure the value you want in the test, after Jest would have reset it:

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]);
});


Another option is to add the following into your package.json to override that configuration, per this issue:

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

This could lead to issues with mock state (calls received) being retained between tests, though, so you'll need to make sure they're getting cleared and/or reset somewhere.


Note that you generally shouldn't mock what you don't own, though - if the interface to @octokit/rest changes your tests will continue to pass but your code won't work. To avoid this issue, I would recommend either or both of:

  • Moving the assertions to the transport layer, using e.g. MSW to check that the right request gets made; or
  • Writing a simple facade that wraps @octokit/rest, decoupling your code from the interface you don't own, and mocking that;

along with higher-level (end-to-end) tests to make sure everything works correctly with the real GitHub API.

In fact, deleting the mocks and writing such a test using 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]);
});

exposes that the current assumption about what octokit.repos.listForOrg would return is inaccurate, because this test fails:

  ● 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)

Your implementation should actually look something more like:

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

or:

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

这篇关于Create React App无法正确模拟__mocks__目录中的模块的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆