测试异步 useEffect

And*_*mdt 13 javascript reactjs jestjs enzyme

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

虽然这在浏览器中工作正常,但测试失败了,因为钩子是异步的,组件没有及时更新。

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

应用程序.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;
Run Code Online (Sandbox Code Playgroud)

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());
  });
});
Run Code Online (Sandbox Code Playgroud)

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

我想这有关系吗?这怎么能解决?

use*_*059 38

好的,所以我想我已经想通了。我现在正在使用最新的依赖项(酶 3.10.0,酶适配器反应 16 1.15.1),我发现了一些令人惊奇的东西。Enzyme 的 mount() 函数似乎返回了一个 promise。我在文档中没有看到任何关于它的内容,但是等待承诺解决似乎可以解决这个问题。使用 react-dom/test-utils 中的行为也是必不可少的,因为它具有使行为起作用的所有新的 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());
});
Run Code Online (Sandbox Code Playgroud)

  • 你救了我的命 (3认同)
  • 我认为 `await Promise.resolve(component);` 没有做任何事情。`mount` 返回一个 `ReactWrapper`,而不是一个承诺。“act”块中的其他行刷新任务队列并更新包装器——这些正在完成工作。 (2认同)
  • 如前所述,可以简化操作块。事实上,它可以写成一行:`await act(() =&gt; Promise.resolve())`。不要忘记之后调用“component.update()”。 (2认同)

Tom*_*ord 7

我遇到了这个问题并遇到了这个线程。我正在对一个钩子进行单元测试,但如果您的异步 useEffect 代码位于组件中,则原则应该是相同的。因为我正在测试一个钩子,所以我renderHook反应钩子测试库调用。如果您正在测试常规组件,则可以根据文档render从调用。react-dom

\n

问题

\n

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

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

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

\n
it("should call an api", async () => {\n        const data = {wibble: "wobble"};\n        axios.mockImplementationOnce(() => Promise.resolve({ data}));\n\n        const { result } = renderHook(() => useMyExampleHook());\n        await new Promise(setImmediate);\n\n        expect(result.current.someState).toMatchObject(data);\n});\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

答案

\n

答案是act()正确使用。文档

\n
\n

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

\n
\n

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

\n
\n    it("should call an api on render and store the result", async () => {\n        const data = { wibble: "wobble" };\n        axios.mockImplementationOnce(() => Promise.resolve({ data }));\n\n        let renderResult = {};\n        await act(async () => {\n            renderResult = renderHook(() => useMyExampleHook());\n        })\n\n        expect(renderResult.result.current.someState).toMatchObject(data);\n    });\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n
\n    it(\'should do a new call when id changes\', async () => {\n        const data1 = { wibble: "wobble" };\n        const data2 = { wibble: "warble" };\n\n        axios.mockImplementationOnce(() => Promise.resolve({ data: data1 }))\n        .mockImplementationOnce(() => Promise.resolve({ data: data2 }));\n\n\n        let renderResult = {};\n        await act(async () => {\n            renderResult = renderHook((id) => useMyExampleHook(id), {\n                initialProps: { id: "id1" }\n            });\n        })\n\n        expect(renderResult.result.current.someState).toMatchObject(data1);\n\n        await act(async () => {\n            renderResult.rerender({ id: "id2" })\n        })\n\n        expect(renderResult.result.current.someState).toMatchObject(data2);\n    })\n
Run Code Online (Sandbox Code Playgroud)\n


Wol*_*ead 6

继@ user2223059 的回答之后,您似乎也可以这样做:

// eslint-disable-next-line require-await     
component = await mount(<MyComponent />);
component.update();
Run Code Online (Sandbox Code Playgroud)

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

  • 其工作原理并不特定于“mount”——当您“等待”一个非承诺时,您仍然可以通过微任务队列进行轮流。这几乎与:`component = mount(...); 相同。等待 Promise.resolve(); 组件.update();` (4认同)