react-testing-library: useEffect not triggering inside jest

  • react-testing-library version: 5.2.3
  • react version: 16.7.0-alpha.0
  • node version: CodeSandbox
  • npm (or yarn) version: CodeSandbox

Relevant code or config:

  let a = false;
  function Test() {
    React.useEffect(() => {
      a = true;
    });
    return <div>Hello World</div>;
  }
  render(<Test />);
  expect(a).toBe(true);

What you did:

I’m trying to test functions with the useEffect hook inside jest.

What happened:

The useEffect hook is not executed correctly inside a jest environment after calling render(<Test />). However it appears to be working correctly if it is called directly in a browser environment.

Reproduction:

Working example: https://codesandbox.io/s/l947z8v6xq (index.js) Not working example in jest: https://codesandbox.io/s/7k5oj92740 (index.test.js)

Problem description:

The useEffect hook should have been executed after the render call

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 16
  • Comments: 32 (16 by maintainers)

Most upvoted comments

That’s because React doesn’t run the hook in sync mode. If you rerender the component the second time, it changes a to true: https://codesandbox.io/s/m5m5yqrr18

For some reason, this workaround:

beforeAll(() => 
  console.log("@ beforeAll")
  jest.spyOn(React, 'useEffect').mockImplementation(React.useLayoutEffect)
)
afterAll(() => React.useEffect.mockRestore())

changes nothing to my Jest tests, effects just never happen. Only manual replacement of useEffect with useLayoutEffect in components does the trick. I know it’s not a QA thread, but maybe other people are experiencing the same… so any help will be appreciated.

Could it be that showAfterDelay only schedules after 500ms, so you need to wait a bit longer than that before running synchronous expects? Try waiting with the mutation observer helper waitForDomChange or one of the other helpers.

import {waitForDomChange} from 'dom-testing-library'

it('should render a spinner after `showAfterDelay` milliseconds', () => {
      // LoadingIndicator uses useEffect internal to schedule the rendering after a delay (setTimeout)
      const { getByTestId, rerender } = render(<LoadingIndicator showAfterDelay={500} />);

      // no spinner initially
      expect(() => getByTestId(LOADING_SPINNER_CONTAINER_TEST_ID)).toThrow();

      // now you need to rerender for React to run the `useEffect` hook
      // this should hopefully be fixed or worked around with some new react-testing-library API
      // this is the price you pay for riding the hype train :)
      // https://github.com/kentcdodds/react-testing-library/issues/215#issuecomment-435592444
      
      await waitForDomChange()

      expect(getByTestId(LOADING_SPINNER_CONTAINER_TEST_ID)).toBeTruthy();
    });

You may also be interesting in http://kcd.im/hooks-and-suspense

I have a video in there that explains one of the big nuances with testing effect hooks.

I do plan on creating a small function called flushEffects or something that’ll make this easier.

Currently the way that I dislike the least for making this work nicely is to put this in my setup file:

beforeAll(() => jest.spyOn(React, 'useEffect').mockImplementation(React.useLayoutEffect))
afterAll(() => React.useEffect.mockRestore())

I don’t like it, but it’s my favorite anyway…

It seems the topic diverged a little bit but going back to the original issue. Why not just use jest.spyOn(React, 'useEffect') and test it this way?

I’m actually starting to think that it’s better to manually trigger effects. That’s why I added the experiential flushEffects function (see the docs).

Noooooooo. That would be an implementation detail. The user don’t actually flush the effects. In fact, they don’t even know what an effect isssss 😕

@kentcdodds Have you considering this workaround approach? Feels much easier imo … https://github.com/facebook/react/issues/14050#issuecomment-438173736

@wzrdzl There is another simple workaround for your case — you just need to rerender it once again 😄

Let me explain how it works (probably I’m wrong):

  • the first render initializes the hook
  • the second render resolves the callback and runs the timer
  • before the third render you need to advance your timer, so when you render it again, your component will already have changed state.

I agree this workaround is ugly, but not sure if it makes sense to fix the case before React core team releases the stable version. Furthermore example with lifecycle alternative works well, so I think jsdom handles React event logic correctly.

I’ve created a repo with the example so you can clone and make sure: https://github.com/sivkoff/hooks-testing-sandbox Unfortunatelly CodeSandbox doesn’t support mocking timers, so you need to clone it to localhost.

@FredyC I followed the documentation you have linked to, but no wrapping of render or event in the act function allows for the effects to asynchronously update the component after its initial render.

Would be great to work together on this issue to get something more obvious working for tests which need to assert side effects on the DOM. At the moment anything in useEffect is untestable if it has an effect on the DOM.

The lack of coverage for my useEffect code led me here. It’s been three months since the issue was closed, but I’m feeling the pain right now. I would absolutely love an automatic flushEffects feature. Without it, how am I supposed to test my side effects? I am not going to introduce timeouts into my tests.

I’m actually starting to think that it’s better to manually trigger effects. That’s why I added the experiential flushEffects function (see the docs).