如何使用Jest在React功能组件中模拟异步调用

时间:2019-11-26 11:54:02

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

我正在测试一个具有提交按钮的功能组件,该按钮可以对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块中,但是为了清楚起见,我将其省略。

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

我遇到的问题是,无论我将其放在第一个代码块的占位符中,由于永远不会到达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();
});

2 个答案:

答案 0 :(得分:0)

尝试类似的操作

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

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

答案 1 :(得分:0)

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

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

选项1

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

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中的回调以告知测试已完成。如果您不调用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()的附加好处,这将有助于使测试警告保持沉默。