使用 React 和 jest 将状态钩子传递给子组件时,如何测试副作用?

时间:2021-04-19 03:33:05

标签: reactjs jestjs

我想测试一个子组件,但它依赖于来自父组件的状态(钩子)。

孩子接收 isModalOpensetIsModalOpen 作为属性。每次 isModalOpen 更改时,useEffect 中的 Child 都会触发。但是,当我尝试单独测试 Child 组件时,我无法强制触发 useEffect 挂钩的状态更改。如何在无需在测试环境中呈现 Parent 的情况下实现这一点?

在下面的示例中,我可以通过渲染 Parent 来使测试通过。我只想渲染 Child,并且仍然让测试通过。

function Child({ isModalOpen, setIsModalOpen }) {
  useEffect(() => {
    console.log("is the modal open?", isModalOpen);
  }, [isModalOpen]);

  return (
    <button data-testId="myBtn" onClick={() => setIsModalOpen(!isModalOpen)}>
      Click to {isModalOpen ? "close" : "open"} modal
    </button>
  );
}

function Parent() {
    const [isModalOpen, setIsModalOpen] = useState(false);
    return <Child isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />;
}

测试:

it("React test", () => {
  let isModalOpen = false;
  const mockFunction = jest.fn((val) => (isModalOpen = val));

  const { queryByTestId } = render(
    <Child isModalOpen={isModalOpen} setIsModalOpen={mockFunction} />
  );

  const btnEl = queryByTestId("myBtn");

  btnEl.click();
  expect(btnEl.innerHTML).toEqual("Click to close modal");

  // -----
  // -----
  // -----
  // ----- The below code works
  // const { queryByTestId } = render(<Parent />);

  // const btnEl = queryByTestId("myBtn");

  // btnEl.click();
  // expect(btnEl.innerHTML).toEqual("Click to close modal");
});

这是上述代码片段的一个工作示例: https://codesandbox.io/s/jest-test-forked-cjm3r?file=/index.test.js

1 个答案:

答案 0 :(得分:2)

实际上总结一下,如果将钩子传递给孩子,那么您要测试的内容与钩子无关。将孩子视为您的对象接受测试;换句话说,它是您唯一要测试的东西。根据定义,“钩子”是无关紧要的。你传入一个函数,你可以断言一个被调用的函数具有正确的值。

总结一下,你有两种情况

  1. 当模态打开并且您单击“单击以关闭模态”按钮时,模态将关闭或以其他方式知道正在使用 false 调用您的模拟函数.
  2. 当模态关闭并且您单击“单击以关闭模态”按钮时,模态将打开或以其他方式知道正在使用 true 调用您的模拟函数.

为了证明这一点,我对您上面的测试进行了小幅修改。随意复制并粘贴它并自行检查,如果您有任何问题,请告诉我。

注意:从沙盒链接复制并粘贴,稍作修改,以免与原始代码产生太大差异。

it("closes the modal when clicking on the button", () => {
    function Child({ isModalOpen, setIsModalOpen }) {
        useEffect(() => {
            console.log("is the modal open?", isModalOpen);
        }, [isModalOpen]);

        return (
            <button data-testId="myBtn" onClick={() => setIsModalOpen(!isModalOpen)}>
                Click to {isModalOpen ? "close" : "open"} modal
            </button>
        );
    }

    // notice the mock here doesn't take arguments
    const mockFunction = jest.fn();

    const { queryByTestId } = render(
        // notice that I've set the initial value for isModal to true
        <Child isModalOpen={true} setIsModalOpen={mockFunction} />
    );

    const btnEl = queryByTestId("myBtn");

    btnEl.click();
    expect(btnEl.innerHTML).toEqual("Click to close modal");

    // the assertion here
    expect(mockFunction).toHaveBeenCalledWith(false);
});

it("opens the modal when clicking on the bottom", () => {
    function Child({ isModalOpen, setIsModalOpen }) {
        useEffect(() => {
            console.log("is the modal open?", isModalOpen);
        }, [isModalOpen]);

        return (
            <button data-testId="myBtn" onClick={() => setIsModalOpen(!isModalOpen)}>
                Click to {isModalOpen ? "close" : "open"} modal
            </button>
        );
    }

    const mockFunction = jest.fn();

    const { queryByTestId } = render(
        // notice that I've set the initial value for isModal to false
        <Child isModalOpen={false} setIsModalOpen={mockFunction} />
    );

    const btnEl = queryByTestId("myBtn");

    btnEl.click();
    expect(btnEl.innerHTML).toEqual("Click to open modal");

    // the assertion here
    expect(mockFunction).toHaveBeenCalledWith(true);
});