【问题标题】:How to limit the scope of Jest mocked functions to a single test如何将 Jest 模拟函数的范围限制为单个测试
【发布时间】:2020-03-12 14:16:39
【问题描述】:

我正在使用 Jest + Testing-Library/React 编写功能测试。经过几天的挠头,我发现当您使用.mockResolvedValue(...).mockResolvedValueOnce(...) 时,模拟的范围不仅限于该测试...

import React from "react";
import { render, waitForElement } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import myApi from '../myApi';
jest.mock('../myApi'); // this will load __mocks__/myApi.js (see below)
import { wait } from '@testing-library/dom';
import App from "../components/App";

afterEach(() => {
  jest.clearAllMocks();
});


describe("App", () => {

    test("first test", async () => {

        myApi.get.mockResolvedValueOnce('FOO');

        // App will call myApi.get() once
        const { container, getByText } = render(<App />);

        await waitForElement(
            () => getByText('FOO')
        );

        expect(myApi.get).toHaveBeenCalledTimes(1);

        // This is going to "leak" into the next test
        myApi.get.mockResolvedValueOnce('BAR');

    });

    test("second test", async () => {

        // This is a decoy! The 'BAR' response in the previous test will be returned
        myApi.get.mockResolvedValueOnce('FOO');

        // App will call myApi.get() once (again)
        const { container, getByText } = render(<App />);

        // THIS WILL FAIL!
        await waitForElement(
            () => getByText('FOO')
        );

        expect(myApi.get).toHaveBeenCalledTimes(1);

    });


});

__mocks__/myApi.js 是这样的:

export default {
  get: jest.fn(() => Promise.resolve({ data: {} }))
};

我了解发生了什么:myApi 被导入到两个测试的共享范围中。这就是为什么.mockResolvedValue*“跨越”测试的原因。

什么是防止这种情况的正确方法?测试应该是原子的,而不是相互耦合的。如果我在first test 中触发另一个get 请求,它应该无法中断second test。那是臭!但正确的模式是什么?我正在考虑将 myApi 的不同“副本”克隆到本地测试范围中......但我担心这会变得很奇怪并导致我的测试信心下降。

我发现 this question 讨论了相同的主题,但只解释了为什么会发生这种情况,而不是讨论避免这种情况的正确模式。

package.json

  "dependencies": {
    "axios": "^0.18.1",
    "moment": "^2.24.0",
    "react": "^16.11.0",
    "react-dom": "^16.11.0",
    "react-redux": "^7.1.3",
    "react-router-dom": "^5.1.2",
    "react-scripts": "2.1.5",
    "redux": "^4.0.4",
    "redux-thunk": "^2.3.0"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^4.2.3",
    "@testing-library/react": "^9.3.2",
    "redux-mock-store": "^1.5.3",
    "typescript": "^3.7.2"
  }

【问题讨论】:

  • easypuppyApi.get 来自哪里?你会分享App 组件吗?
  • @Teneff 抱歉,这是一个复制/粘贴错误。我修复了它,所以变量名现在是 myApi
  • 我认为&lt;App /&gt; 的内容并不重要,因为我的问题是关于 Jest。我认为它只调用myApi.get() 才重要,对吗?

标签: unit-testing jestjs integration-testing react-testing-library


【解决方案1】:

这就是我构建测试的方式:

  • 在测试服的开头有一个beforeAll
    • 设置模拟
    • 清除即将测试的模拟函数
    • 渲染组件
    • 调用函数
  • 编写单独的测试用例,每个测试用例在自己的test/it 块中描述
    • 让每个人都有更好的、单独的描述
    • 更好地了解--verbose 模式下的失败情况

示例:

App.spec.js
describe("App", () => {
  // jest allows nesting of test suits
  // allowing us to have prettier reporting
  // and having scoped variables
  describe("Api.get returning FOO", () => {
    // define variables used in the test suit
    let wrapper;
    // having the setup here
    beforeAll(async () => {
      Api.get.mockClear();
      Api.get.mockResolvedValue("FOO");
      const { container, getByText } = render(<App />);
      // expose the container to the scope
      wrapper = container;

      await waitForElement(() => getByText("FOO"));
    });

    // write the test cases balow
    // each assertion in a separate test block
    test("should call the Api once", () => {
      expect(Api.get).toHaveBeenCalledOnce();
    });

    test("should have been called with data", () => {
      expect(Api.get).toHaveBeenCalledWith({ x: "y" });
    });

    test("should match the snapshot", () => {
      expect(wrapper).toMatchSnapshot();
    });
  });

  describe("Api.get returning BAR", () => {
    // define variables used in the test suit
    let wrapper;

    beforeAll(async () => {
      Api.get.mockClear();
      Api.get.mockResolvedValue("BAR");
      const { container, getByText } = render(<App />);
      // expose the container to the scope
      wrapper = container;

      await waitForElement(() => getByText("FOO"));
    });

    test.todo("describe what is supposed to happen with Api.get");
    test.todo("describe what is supposed to happen container");
  });
});

回到问题 - 是的,模拟函数将在整个测试文件中使用,但如果您有尚未消耗的 mockResolvedValueOnce(泄漏到下一个测试中),上述任何一个测试用例都会失败或者你的测试写得不好。


编辑:

作为一个思想实验,你能想出一个可以“解决”这个问题的结构吗?

要在每次测试后删除返回的模拟值和实现,您可以使用

afterEach(() => {
  jest.resetAllMocks()
});

【讨论】:

  • 感谢详尽的示例...我喜欢嵌套的 describe 块。在大多数示例中您看不到这一点,但这是有道理的。我理解你关于模拟泄漏到下一个测试中以某种方式导致失败的观点。但是,一个测试中的一个问题导致另一个测试失败仍然让人觉得有问题。我们不希望他们独立吗?作为一个思想实验,你能想出一个可以“解决”这个问题的结构吗?
  • @emersonthis 如果我们想让它们独立,我们只需将它们写在单独的文件中
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-04
  • 2019-02-09
相关资源
最近更新 更多