测试异步使用效果

时间:2019-07-12 11:46:10

标签: javascript reactjs jestjs enzyme

我的功能组件使用useEffect挂钩从挂载的API中获取数据。我希望能够测试提取的数据是否正确显示。

虽然这在浏览器中可以正常工作,但由于挂钩是异步的,因此组件无法及时更新,因此测试失败。

实时代码: https://codesandbox.io/s/peaceful-knuth-q24ih?fontsize=14

App.js

import React from "react";

function getData() {
  return new Promise(resolve => {
    setTimeout(() => resolve(4), 400);
  });
}

function App() {
  const [members, setMembers] = React.useState(0);

  React.useEffect(() => {
    async function fetch() {
      const response = await getData();

      setMembers(response);
    }

    fetch();
  }, []);

  return <div>{members} members</div>;
}

export default App;

App.test.js

import App from "./App";
import React from "react";
import { mount } from "enzyme";

describe("app", () => {
  it("should render", () => {
    const wrapper = mount(<App />);

    console.log(wrapper.debug());
  });
});

除此之外,Jest发出警告说: Warning: An update to App inside a test was not wrapped in act(...).

我想这是相关的吗?如何解决?

5 个答案:

答案 0 :(得分:2)

好的,所以我想我已经解决了。我现在正在使用最新的依赖项(酶3.10.0,酶-适配器-反应16 1.15.1),并且发现了一些令人惊奇的东西。酶的mount()函数似乎返回了诺言。我在文档中没有看到任何有关它的信息,但是等待该承诺解决似乎可以解决此问题。使用react-dom / test-utils中的act也是必不可少的,因为它具有所有新的React魔术来使行为起作用。

it('handles async useEffect', async () => {
    const component = mount(<MyComponent />);
    await act(async () => {
        await Promise.resolve(component);
        await new Promise(resolve => setImmediate(resolve));
        component.update();
    });
    console.log(component.debug());
});

答案 1 :(得分:1)

这是救生员

    export const waitForComponentToPaint = async (wrapper: any) => {
  await act(async () => {
    await new Promise(resolve => setTimeout(resolve, 0));
    await wait(0);
    wrapper.update();
  });
};

正在测试

await waitForComponentToPaint(wrapper);

答案 2 :(得分:1)

我遇到了这个问题并遇到了这个线程。我正在对钩子进行单元测试,但如果您的异步 useEffect 代码在组件中,则原理应该相同。

问题

假设您有一个在挂载上执行一些异步工作的 React 钩子或组件,并且您想对其进行测试。它可能看起来有点像这样:

const useMyExampleHook = id => {
    const [someState, setSomeState] = useState({});
    useEffect(() => {
        const asyncOperation = async () => {
            const result = await axios({
                url: `https://myapi.com/${id}`,
                method: "GET"
            });
            setSomeState(() => result.data);
        }

        asyncOperation();

    }, [id])
    return { someState }
}

到目前为止,我一直在对这些钩子进行单元测试:

it("should call an api", async () => {
        const data = {wibble: "wobble"};
        axios.mockImplementationOnce(() => Promise.resolve({ data}));

        const { result } = renderHook(() => useMyExampleHook());
        await new Promise(setImmediate);

        expect(result.current.someState).toMatchObject(data);
});

并使用 await new Promise(setImmediate); 来“刷新”承诺。这适用于像我上面的简单测试,但当我们开始在一个测试中对钩子/组件进行多次更新时,似乎会在测试渲染器中引起某种竞争条件。

答案

答案是正确使用 act()The docs

<块引用>

在编写[单元测试]时... react-dom/test-utils 提供了一个名为 act() 的帮助程序,可确保在您做出任何断言之前,与这些“单元”相关的所有更新都已处理并应用于 DOM .

所以我们简单的测试代码实际上是这样的:


    it("should call an api on render and store the result", async () => {
        const data = { wibble: "wobble" };
        axios.mockImplementationOnce(() => Promise.resolve({ data }));

        let renderResult = {};
        await act(async () => {
            renderResult = renderHook(() => useMyExampleHook());
        })

        expect(renderResult.result.current.someState).toMatchObject(data);
    });

关键的区别在于异步操作围绕钩子的初始渲染。这确保在我们开始尝试检查状态之前 useEffect 钩子已经完成了它的工作。如果我们需要更新钩子,这个动作也会被包裹在它自己的动作块中。

更复杂的测试用例可能如下所示:


    it('should do a new call when id changes', async () => {
        const data1 = { wibble: "wobble" };
        const data2 = { wibble: "warble" };

        axios.mockImplementationOnce(() => Promise.resolve({ data: data1 }))
        .mockImplementationOnce(() => Promise.resolve({ data: data2 }));


        let renderResult = {};
        await act(async () => {
            renderResult = renderHook((id) => useMyExampleHook(id), {
                initialProps: { id: "id1" }
            });
        })

        expect(renderResult.result.current.someState).toMatchObject(data1);

        await act(async () => {
            renderResult.rerender({ id: "id2" })
        })

        expect(renderResult.result.current.someState).toMatchObject(data2);
    })

答案 3 :(得分:0)

按照@ user2223059的回答,它看起来也可以:

// eslint-disable-next-line require-await     
component = await mount(<MyComponent />);
component.update();

不幸的是,您需要eslint-disable-next-line,因为否则它会警告不必要的等待...但是删除等待会导致错误的行为。

答案 4 :(得分:0)

经过大量实验后,我想出了一个最终对我有用的解决方案,希望能满足您的目的。

见下文

import React from "react";
import { mount } from "enzyme";
import { act } from 'react-dom/test-utils';
import App from "./App";

describe("app", () => {
  it("should render", async () => {
    const wrapper = mount(<App />);

    await new Promise((resolve) => setImmediate(resolve));
    await act(
      () =>
        new Promise((resolve) => {
          resolve();
        })
    );
    wrapper.update();
    // data loaded
  });
});