【问题标题】:How to mock async call in React functional component using jest如何使用 jest 在 React 功能组件中模拟异步调用
【发布时间】:2019-11-26 11:54:02
【问题描述】:

我正在测试一个具有提交按钮的功能组件,该按钮可以对 api 进行异步调用。异步调用位于自定义挂钩中。根据标准测试实践,我已经模拟了钩子,因此我的模拟将被调用而不是实际的异步 api:

someComponent.test.js

jest.mock("../../../CustomHooks/user", () => ({
  useUser: () => ({
    error: null,
    loading: false,
    forgotPassword: <SOMETHING HERE>

  })
}));

我知道我的 forgotPassword 函数被调用,因为当我将其更改为 forgotPassword: "" 时,我在测试中收到错误,指出 forgotPassword 不是函数。

单击提交按钮时调用的函数的一个非常简单的表示是这样的:

someComponent.js

import { useUser } from "../../../CustomHooks/user"

const SomeComponent = () => {

  ....state and other things etc....

  const { error, loading, forgotPassword } = useUser()

  const submit = async () => {
      await forgotPassword(emailValue);
      setState(prevState => {
        return {
          ...prevState,
          content: "code"
        };
      });
  }

}

注意:我对异步函数 await forgotPassword... 的调用包含在代码中的 try/catch 块中,但为了清楚起见,我将其省略了。

在生产中,当提交按钮被按下时,会发生异步调用,然后应该切换状态,从而渲染一些其他组件。我的测试查看这些组件是否已被渲染(我为此使用了反应测试库)。

我遇到的问题是,无论我在第一个代码块的占位符中放置什么,我的测试总是会失败,因为永远不会到达 setState 块。如果我删除 await 语句,那么 setState 块会被击中,并且我想要出现的组件在状态发生变化时就在那里。但是,显然这在测试之外不会按预期工作,因为实际调用是异步的。以下是我尝试过的一些不起作用的方法:

DOESN'T WORK

forgotPassword: () => {
      return Promise.resolve({ data: {} });
    }
DOESN'T WORK

forgotPassword: jest.fn(() => {
      return Promise.resolve();
    })
DOESN'T WORK

forgotPassword: jest.fn(email => {
      return new Promise((resolve, reject) => {
        if (email) {
          resolve(email);
        } else {
          reject("Error");
        }
      });
    }),

正如我已经说过的,如果我删除 await 语句,那么状态会发生变化并且组件会出现,因此测试通过。但是,由于显而易见的原因,这不是我想要的。

额外信息

这是我的测试的简化版本:

it("changes state/content from email to code when submit clicked", () => {
  const { getByTestId, getByText, debug } = render(<RENDER THE COMPONENT>);

  const submitButton = getByTestId("fpwSubmitButton");
  expect(submitButton).toBeInTheDocument();

  const emailInput = getByTestId("fpwEmailInput");

  fireEvent.change(emailInput, {
    target: { value: "testemail@testemail.com" }
  });

  fireEvent.click(submitButton);

  debug();

  THE STATEMENTS BELOW ARE WHERE IT FAILS AS THE STATE DOESN'T CHANGE WHEN AWAIT IS PRESENT

  const codeInput = getByTestId("CodeInput");
  expect(codeInput).toBeInTheDocument();
});

【问题讨论】:

    标签: reactjs unit-testing testing async-await jestjs


    【解决方案1】:

    对于遇到同样问题的任何人,我发现了三种可以解决此问题的方法(首选方法是选项 3)。所有方法都使用一个简单的模拟函数来替换我问题中第一个代码块的&lt;SOMETHING HERE&gt;。这可以替换为() =&gt; {}:

    jest.mock("../../../CustomHooks/user", () => ({
      useUser: () => ({
        error: null,
        loading: false,
        forgotPassword: () => {}
      })
    }));
    

    选项 1

    第一种方法是使用 done 回调将依赖于异步函数的测试代码包装在 setTimeout 中:

    it("changes state/content from email to code when submit clicked", done => {
      const { getByTestId, debug } = render(<RENDER THE COMPONENT>);
    
      const submitButton = getByTestId("fpwSubmitButton");
      expect(submitButton).toBeInTheDocument();
    
      const emailInput = getByTestId("fpwEmailInput");
    
      fireEvent.change(emailInput, {
        target: { value: "testemail@testemail.com" }
      });
    
      fireEvent.click(submitButton);
    
      setTimeout(() => {
        const codeInput = getByTestId("CodeInput");
        expect(codeInput).toBeInTheDocument();
        done();
      });
    
      debug();
    });
    
    

    注意顶部的done 回调,以及底部包裹在setTimeout 中的测试代码,然后在setTimeout 中调用回调告诉jest 测试已完成。如果您不调用done 回调,测试将失败,因为它会超时。

    选项 2

    第二种方法是使用一个名为flushPromises()的函数:

    function flushPromises() {
      return new Promise(resolve => setImmediate(resolve));
    }
    
    it("changes state/content from email to code when submit clicked", async () => {
      const { getByTestId, debug } = render(<RENDER THE COMPONENT>);
    
      const submitButton = getByTestId("fpwSubmitButton");
      expect(submitButton).toBeInTheDocument();
    
      const emailInput = getByTestId("fpwEmailInput");
    
      fireEvent.change(emailInput, {
        target: { value: "testemail@testemail.com" }
      });
    
      fireEvent.click(submitButton);
    
      await flushPromises();
    
      const codeInput = getByTestId("CodeInput");
      expect(codeInput).toBeInTheDocument();
    
      debug();
    });
    

    注意顶部的flushPromises() 函数,然后是底部的调用站点。

    选项 3(首选方法)

    最后一种方法是从 react-testing-library 导入 wait,将测试设置为异步,然后在有异步代码时 await wait()

    ...
    import { render, fireEvent, cleanup, wait } from "@testing-library/react";
    
    ...
    
    it("changes state/content from email to code when submit clicked", async () => {
      const { getByTestId, debug } = render(<RENDER THE COMPONENT>);
    
      const submitButton = getByTestId("fpwSubmitButton");
      expect(submitButton).toBeInTheDocument();
    
      const emailInput = getByTestId("fpwEmailInput");
    
      fireEvent.change(emailInput, {
        target: { value: "testemail@testemail.com" }
      });
    
      fireEvent.click(submitButton);
    
      await wait()
    
      const codeInput = getByTestId("CodeInput");
      expect(codeInput).toBeInTheDocument();
    
      debug();
    });
    

    所有这些解决方案都有效,因为它们在执行测试代码之前等待下一个事件循环。 Wait() 基本上是 flushPromises() 的一个包装器,还有一个额外的好处是包含 act(),这将有助于消除测试警告。

    【讨论】:

      【解决方案2】:

      试试这样的

      forgotPassword: jest.fn( async email => {
          return await new Promise( ( resolve, reject ) => {
              if ( email ) {
                  resolve( email );
              } else {
                  reject( "Error" );
              }
          } );
      } );
      

      如果不起作用,请告诉我。

      【讨论】:

      • 非常感谢您的回复!我已经尝试过您的建议,但不幸的是它没有奏效......我已经编辑了我的问题并稍微填充了组件定义,所以它更清楚一点,不确定这是否会有所帮助
      猜你喜欢
      • 2022-08-09
      • 2021-09-14
      • 2020-02-25
      • 2021-01-10
      • 1970-01-01
      • 2019-09-18
      • 2021-05-10
      • 2021-08-11
      • 1970-01-01
      相关资源
      最近更新 更多