Reactjs 如何使用jest模拟React功能组件中的异步调用

Reactjs 如何使用jest模拟React功能组件中的异步调用,reactjs,unit-testing,testing,async-await,jestjs,Reactjs,Unit Testing,Testing,Async Await,Jestjs,我正在测试一个功能组件,它有一个提交按钮,可以对api进行异步调用。异步调用位于自定义挂钩中。根据标准测试实践,我模拟了钩子,因此将调用我的模拟,而不是实际的异步api: someComponent.test.js jest.mock("../../../CustomHooks/user", () => ({ useUser: () => ({ error: null, loading: false, forgotPassword: <SOMETH

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

someComponent.test.js

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

  })
}));
注意:我对异步函数
wait forgotPassword…
的调用被包装在我的代码中的try/catch块中,但为了清楚起见,我省略了这一点

在生产环境中,当按下submit按钮时,会发生异步调用,然后应切换状态,从而呈现一些其他组件。我的测试查看这些组件是否已呈现(我正在为此使用react测试库)

我遇到的问题是,无论我在第一个代码块的占位符中放置什么,我的测试都会失败,因为永远不会到达setState块。如果我删除
await
语句,则会命中setState块,并且当状态发生更改时,我要显示的组件就在那里。但是,很明显,在测试之外,这不会像预期的那样工作,因为实际调用是异步的。以下是我尝试过的一些不起作用的方法:

DOESN'T WORK

forgotPassword: () => {
      return Promise.resolve({ data: {} });
    }
正如我已经说过的,如果我删除
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();
});
it(“单击提交时将状态/内容从电子邮件更改为代码”,()=>{
const{getByTestId,getByText,debug}=render();
const submitButton=getByTestId(“fpwSubmitButton”);
expect(submitButton).toBeInTheDocument();
const emailInput=getByTestId(“fpwEmailInput”);
fireEvent.change(电子邮件输入{
目标:{值:”testemail@testemail.com" }
});
fireEvent.单击(提交按钮);
调试();
下面的语句是它失败的地方,因为当wait出现时状态没有改变
const codeInput=getByTestId(“codeInput”);
expect(codeInput.toBeInTheDocument();
});

试试这样的方法

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

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

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

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

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

选项3(首选方法)

最后一种方法是从react测试库导入wait,将测试设置为异步,然后
wait 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();
});
。。。
从“@testing library/react”导入{render,firevent,cleanup,wait}”;
...
它(“单击提交时将状态/内容从电子邮件更改为代码”,异步()=>{
const{getByTestId,debug}=render();
const submitButton=getByTestId(“fpwSubmitButton”);
expect(submitButton).toBeInTheDocument();
const emailInput=getByTestId(“fpwEmailInput”);
fireEvent.change(电子邮件输入{
目标:{值:”testemail@testemail.com" }
});
fireEvent.单击(提交按钮);
等待等待
const codeInput=getByTestId(“codeInput”);
expect(codeInput.toBeInTheDocument();
调试();
});

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

非常感谢您的回复!我已经尝试了你的建议,但不幸的是没有成功。。。我已经编辑了我的问题,并对组件定义进行了填充,使其更加清晰,但不确定这是否会有所帮助
jest.mock("../../../CustomHooks/user", () => ({
  useUser: () => ({
    error: null,
    loading: false,
    forgotPassword: () => {}
  })
}));
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();
});

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