Testing asynchronous useEffect
Asked Answered
R

7

22

My functional component uses the useEffect hook to fetch data from an API on mount. I want to be able to test that the fetched data is displayed correctly.

While this works fine in the browser, the tests are failing because the hook is asynchronous the component doesn't update in time.

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

Besides that, Jest throws a warning saying: Warning: An update to App inside a test was not wrapped in act(...).

I guess this is related? How could this be fixed?

Ralli answered 12/7, 2019 at 11:46 Comment(5)
It's a known issue - github.com/testing-library/react-testing-library/issues/281Gazelle
Also check this - codesandbox.io/s/k14k63y03vGazelle
Okay, I saw the same issue but assumed it would be related to react-testing-library, therefore not having a relation with Enzyme. Seems like I can't do anything right now, thanks for the clarification.Ralli
try act(() => { wrapper = mount(<App />); }). React docs say it does some magic under the hood reactjs.org/blog/2019/02/06/react-v16.8.0.htmlHumidistat
Yeah, I already tried using it after reading some comments on GitHub. Unfortunately, the result is the same...Ralli
C
41

Ok, so I think I've figured something out. I'm using the latest dependencies right now (enzyme 3.10.0, enzyme-adapter-react-16 1.15.1), and I've found something kind of amazing. Enzyme's mount() function appears to return a promise. I haven't seen anything about it in the documentation, but waiting for that promise to resolve appears to solve this problem. Using act from react-dom/test-utils is also essential, as it has all the new React magic to make the behavior work.

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());
});
Combustion answered 2/11, 2019 at 14:25 Comment(7)
You save my lifeOliphant
You my friend are absolute genius. This was driving me absolutely nuts and I tried a million approaches. You deserve a medal for this. Thank you. This worked me on a Preact project just so others know.Rounder
Excellent solution. Also worth noting to pop this into a reusable util function to use in other tests for anyone coming across this answer.Kenward
omg, i'll give you medal of peace! will always remember this solution. Thanksss!!!!!!Afton
I don't think await Promise.resolve(component); does anything. mount returns a ReactWrapper, not a promise. The other lines in your act block flush the task queue and update the wrapper -- those are doing the work.Thickset
What you are saying here as @Thickset said is not correct, mount does not return a promise, you are essentially just creating a promise and resolve it immediately with Promise.resolve(component) where the resolved value is the wrapper you could replace it with Promise.resolve() and it will still work. also no need for the second await new Promise(resolve => setImmediate(resolve)). What makes it work is that once you added await that made the following code ran after react internal tasks were done.Megaton
As stated, the act block can be simplified. In fact, it can be written as a one-liner: await act(() => Promise.resolve()). Don't forget to call component.update() after.Chorography
L
8

I was having this problem and came across this thread. I'm unit testing a hook but the principles should be the same if your async useEffect code is in a component. Because I'm testing a hook, I'm calling renderHook from react hooks testing library. If you're testing a regular component, you'd call render from react-dom, as per the docs.

The problem

Say you have a react hook or component that does some async work on mount and you want to test it. It might look a bit like this:

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 }
}

Until now, I've been unit testing these hooks like this:

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

and using await new Promise(setImmediate); to "flush" the promises. This works OK for simple tests like my one above but seems to cause some sort of race condition in the test renderer when we start doing multiple updates to the hook/component in one test.

The answer

The answer is to use act() properly. The docs say

When writing [unit tests]... react-dom/test-utils provides a helper called act() that makes sure all updates related to these “units” have been processed and applied to the DOM before you make any assertions.

So our simple test code actually wants to look like this:


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

The crucial difference is that async act around the initial render of the hook. That makes sure that the useEffect hook has done its business before we start trying to inspect the state. If we need to update the hook, that action gets wrapped in its own act block too.

A more complex test case might look like this:


    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);
    })
Lederer answered 15/3, 2021 at 11:34 Comment(4)
The great answer for a cases since you have a custom hook!! Thank you =)Thrawn
You did not define or addressed what is renderHookCreolacreole
Sorry @Creolacreole see here: react-hooks-testing-library.com/reference/api#renderhookLederer
You should also update your answer to address this mysterious reference :)Creolacreole
S
7

Following on from @user2223059's answer it also looks like you can do:

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

Unfortunately you need the eslint-disable-next-line because otherwise it warns about an unnecessary await... yet removing the await results in incorrect behaviour.

Sycee answered 28/11, 2019 at 9:28 Comment(2)
the reason this works isn't specific to mount - when you await a non-promise, you still get a turn through the microtask queue. this is almost identical to: component = mount(...); await Promise.resolve(); component.update();Drops
It only works by pure luck. sometimes.Creolacreole
S
2

I was also facing similar issue. To solve this I have used waitFor function of React testing library in enzyme test.

import { waitFor } from '@testing-library/react';

it('render component', async () => {

    const wrapper = mount(<Component {...props} />);
    await waitFor(() => {
       wrapper.update();
       expect(wrapper.find('.some-class')).toHaveLength(1);
    }
});

This solution will wait for our expect condition to fulfill. Inside expect condition you can assert on any HTML element which get rendered after the api call success.

Stepaniestepbrother answered 9/5, 2022 at 10:17 Comment(0)
S
1

this is a life saver

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

in test

await waitForComponentToPaint(wrapper);
Steelwork answered 15/5, 2020 at 8:53 Comment(2)
Where do you get wait from?Gainor
I'm guessing it's just a wrapper for setTimeout. My implementation looks like this export const timeout = (ms: number) => new Promise((resolve) => setTimeout(() => resolve(true), ms));Ryswick
H
0

After lots of experimentation I have come up with a solution that finally works for me, and hopefully will fit your purpose.

See below

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
  });
});
Heirloom answered 18/5, 2021 at 17:4 Comment(0)
S
0

There are lots of answers here that are correct, but I want to emphasize an important commonality in all of the answers... which is that they're using enzyme's mount. I found out the hard way that using shallow will not work!

This might seem like a minor detail, but in researching this problem I also came across a lot of advice (such as this blog post) to use the render method from @testing-library/react - that's ok, but IMO testing with enzyme is much more useful, because it allows you to access the find method to make assertions much more clearly about your component's children.

I eventually got this working using a relatively clean looking version from @Supriya Gole's answer. To pile on, here's how my code looks :

import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { MyComponent } from '../MyComponent';
import { SubComponent } from '../SubComponent';

describe('MyComponent', () => {
  const defaultProps = { bla: 'bla' };

  it('does stuff', () => {
    const component = mount(<MyComponent {...defaultProps} />);
    await waitFor(() => {
      component.update();
      expect(component.find(SubComponent)).toHaveLength(1);
    });
    shallowSnapshot(<MyComponent {...defaultProps} />);
  });
});
Seascape answered 22/3, 2023 at 2:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.