Search code examples
reduxreact-reduxreact-testing-libraryredux-toolkit

Async thunks cause warnings in tests (wrapped in act)


When trying to test the component which dispatches async thunk I get the following warnings. They are displayed because of updates performed after the test is finished.

  console.error
    Warning: An update to App inside a test was not wrapped in act(...).
    
    When testing, code that causes React state updates should be wrapped into act(...):
    
    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */
    
    This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
        at App (/home/karlosos/Dev/nokia/playground/testing-redux/src/App.tsx:6:34)
        at Provider (/home/karlosos/Dev/nokia/playground/testing-redux/node_modules/react-redux/lib/components/Provider.js:19:3)
        at Wrapper (/home/karlosos/Dev/nokia/playground/testing-redux/src/testUtils.tsx:11:22)

      at printWarning (node_modules/react-dom/cjs/react-dom.development.js:86:30)
      at error (node_modules/react-dom/cjs/react-dom.development.js:60:7)
      at warnIfUpdatesNotWrappedWithActDEV (node_modules/react-dom/cjs/react-dom.development.js:27589:9)
      at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom.development.js:25508:5)
      at forceStoreRerender (node_modules/react-dom/cjs/react-dom.development.js:16977:5)
      at Object.handleStoreChange [as callback] (node_modules/react-dom/cjs/react-dom.development.js:16953:7)
      at node_modules/react-redux/lib/utils/Subscription.js:23:20

This is explanation why warnings are visible. enter image description here

One way of fixing them is to introduce some kind of barrier that will wait for all pending async actions to be finished. But doing that my tests would need to have asserts for logic that I don't want to test.

enter image description here

I have recreated a minimal reproducible project here: https://github.com/karlosos/react-redux-async-warnings/tree/main/src

My example test looks like this:

  test('WHEN component rendered THEN counter value is being loaded', () => {
    // WHEN
    renderWithProviders(<App />)

    // THEN
    expect(Api.getValue).toHaveBeenCalledTimes(1);
    const loadingSpinner = screen.getByTestId('loading-spinner');
    expect(loadingSpinner).toBeInTheDocument();

    // things will happen to the component here after test is done
    // precisely the data fetched from API will be displayed
  });

and example thunk:

export const fetchCounterValue = (): AppThunk => async (dispatch, getState) => {
  if (getState().counter.fetchValueStatus === "loading") {
    return;
  }

  dispatch(fetchValueStart());
  try {
    const result = await Api.getValue();
    dispatch(fetchValueSuccess(result));
  } catch (e) {
    dispatch(fetchValueError('Could not fetch the data'));
  }
};

Update 1 - No warnings when using waitFor at the end of the test

When I've added await waitFor(() => new Promise(res => setTimeout(res, 0))); at the end of the test then warnings are not visible. But I don't want to edit every single test case. It seems like a hack.

  test('WHEN component rendered THEN counter value is being loaded', async () => {
    // WHEN
    renderWithProviders(<App />)

    // THEN
    expect(Api.getValue).toHaveBeenCalledTimes(1);
    const loadingSpinner = screen.getByTestId('loading-spinner');
    expect(loadingSpinner).toBeInTheDocument();

    await waitFor(() => new Promise(res => setTimeout(res, 0)));
  });

Solution

  • The correct solution is not to set global.IS_REACT_ACT_ENVIRONMENT = false;, but to make the correct test case in the first place. This act(...) warning means that there are some updates happening to the component after the test has finished.

    In this particular scenario, it is caused by the loading spinner disappearing and then data being displayed. To fix this test scenario, we need to wait for this loading spinner to disappear or alternatively wait for data to appear on the screen.

    This is what a correct test case should look like:

      test('WHEN component rendered THEN counter value is being loaded', async () => {
        // WHEN
        renderWithProviders(<App />)
    
        // THEN
        expect(Api.getValue).toHaveBeenCalledTimes(1);
        const loadingSpinner = screen.getByTestId('loading-spinner');
        expect(loadingSpinner).toBeInTheDocument();
    
        await waitForElementToBeRemoved(() => screen.queryByTestId('loading-spinner')); // This is preventing `act(...)` warnings
      });
    

    Learn more about act(...) warnings here: