Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
384 views
in Technique[技术] by (71.8m points)

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

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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...