admin管理员组

文章数量:1302406

Component:

const MyComponent = props => {
  const {price} = props;
  const result1 = useResult(price);

  return (
    <div>...</div>
  )
}     

Custom Hook:

export const useResult = (price) => {
  const [result, setResult] = useState([]);

  useEffect(() => {
    const data = [{price: price}]
    setResult(data);        
  }, [price]);

  return result;
};

Jest test:

  it('should ...', async () => {
    render(
        <MyComponent price={300}/>)
    )
    await waitFor(() => {
      expect(...).toBeInTheDocument();
    });
  });

What it does happen with the above code is that MyComponent, when running the test, renders only once instead of two (when the application runs). After the initial render where result1 is an empty array, useEffect of useResult is running and since there is a state change due to setResult(data), I should expect MyComponent to be re-rendered. However, that's not the case and result1 still equals to [] whereas it should equal to [{price:300}].

Hence, it seems custom hooks under testing behave differently than the real app. I thought it would be okay to test them indirectly through the ponent that calls them.

Any explanation/thoughts for the above?

UPDATE

The issue that invoked the above erroneous behaviour was state mutation!! It worked with the app but not with the test! My mistake was to attempt to use push in order to add an element to an array that was a state variable...

Component:

const MyComponent = props => {
  const {price} = props;
  const result1 = useResult(price);

  return (
    <div>...</div>
  )
}     

Custom Hook:

export const useResult = (price) => {
  const [result, setResult] = useState([]);

  useEffect(() => {
    const data = [{price: price}]
    setResult(data);        
  }, [price]);

  return result;
};

Jest test:

  it('should ...', async () => {
    render(
        <MyComponent price={300}/>)
    )
    await waitFor(() => {
      expect(...).toBeInTheDocument();
    });
  });

What it does happen with the above code is that MyComponent, when running the test, renders only once instead of two (when the application runs). After the initial render where result1 is an empty array, useEffect of useResult is running and since there is a state change due to setResult(data), I should expect MyComponent to be re-rendered. However, that's not the case and result1 still equals to [] whereas it should equal to [{price:300}].

Hence, it seems custom hooks under testing behave differently than the real app. I thought it would be okay to test them indirectly through the ponent that calls them.

Any explanation/thoughts for the above?

UPDATE

The issue that invoked the above erroneous behaviour was state mutation!! It worked with the app but not with the test! My mistake was to attempt to use push in order to add an element to an array that was a state variable...

Share Improve this question edited Nov 18, 2022 at 17:56 Unknown developer asked Nov 8, 2022 at 19:10 Unknown developerUnknown developer 5,97017 gold badges60 silver badges118 bronze badges 10
  • const data = ... //we build an array somehow - is this a synchronous operation? – Konrad Commented Nov 8, 2022 at 19:14
  • Yes. It is a synchronous one. – Unknown developer Commented Nov 8, 2022 at 19:20
  • 1 Tests are synchronous, React state updates are not synchronously processed. The test needs to wait for the ponent to rerender with any updated UI you are trying to assert on. – Drew Reese Commented Nov 8, 2022 at 19:52
  • Okay. However, what's the answer to my question? When debugging the test, there is no re-rendering of the ponent. Why? – Unknown developer Commented Nov 8, 2022 at 21:28
  • 2 What's the goal of your test from a user's point of view? The user won't really care how many times it re-renders. I'd suggest filling out both the code and test a bit more to give us more insights into what you're actually trying to test. – Cathal Mac Donnacha Commented Nov 9, 2022 at 0:15
 |  Show 5 more ments

4 Answers 4

Reset to default 3

Well, it seems that you are asking a very specific thing about testing a custom hook. In that case, I also had some issues in the past testing custom hooks through @testing-library and a different package was created (and recently incorporated into the @testing-library) that provides the renderHook() function for testing custom hooks. I suggest you to test that.

  • Original Package (do not use it. Use directly the TL one)
  • Docs about the renderHook() call inside the TL docs

You can read more about it in this blog post from Kent C. Dodds.

I also suggest you create a "state change" to test your ponent and test the hook with the renderHook().

Here is a simple codesandbox with some tests for a ponent similar to your case.

Original Answer

Essentially, your test is not waiting for the ponent to perform the side effects. There are 2 ways of waiting for that:

  • Using waitFor()
import { waitFor, screen } from '@testing-library/react'

// ...
  // add the `async` before the callback function
  it('should ...', async () => {
    render(<MyComponent price={300}/>);
    
    await waitFor(() =>
      expect(screen.getByText('your-text-goes-here')).toBeInTheDocument()
    )
  });
  • Using the findBy* query from RTL, that returns a Promise (read the Docs here) and is a bination from the waitFor and getBy* query (read docs here)
import { screen } from '@testing-library/react'

// ...
  // add the `async` before the callback function
  it('should ...', async () => {
    render(<MyComponent price={300}/>);

    expect(await screen.findByText('your-text-goes-here')).toBeInTheDocument();
  });

Step 1: the code being tested

If, as mentioned in the ments of the question, the operation inside the effect is synchronous, then using useEffect for setting this state based on the props is undesirable in all cases. Not only for testing.

The ponent will render, update the DOM and immediately need to re render the following frame because it's state was updated. It causes a flash effect for the user and needlessly slows the app down.

If the operation is cheap, it's way more efficient to just execute it on every render.

If the operation can be more expensive, you can wrap it in useMemo to ensure it only happens when there's changes to the inputs.

export const useResult = (price) => {
  return useMemo(
    // I assume this is a stub for a expensive operation.
    () => [{price: price}],
    [price]
  );
};

If, for some obscure reason, you do need to do this in an effect anyway (you probably don't but there's edge cases), you can use a layoutEffect instead. It will be processed synchronously and avoid the flashing frame. Still wouldn't remend it but it's a slight improvement over a regular effect.

Step 2: Testing

If you changed the ponent to not use an effect, it should now be correct from the first render, and you don't have the problem anymore. Avoiding having a problem in the first place is also a valid solution :D

If you do find the need to flush something synchronously in a test, there's now the flushSync function which does just that.

Perhaps it would also flush the state update in the effect, causing your test to work with no other changes. I guess it should, as new updates triggered by effects while flushing should continue to be processed before returning.

flushSync(() => {
  render(
    <MyComponent price={300}/>)
  )
})

In any case there's no point doing this if you can instead improve the ponent to fix the additional render introduced by setting state in an effect.

you can do:

The test will have to be async: it('should ...',  async() => { ....

await screen.findByText('whatever');
This is async so it will wait to find whatever and fail if it can't find it

or you can do
await waitFor (() => {
   const whatever = screen.getByText('whatever');
   expect(whatever).toBeInTheDocument();
})

You are not waiting for the ponent to be rerendered

import { waitFor, screen } from 'testing-library/react'

it('should ...',  async () => {
    render(
        <MyComponent price={300}/>)
    )
    
    await waitFor (() => {
        // check that props.price is shown
        screen.debug() // check what's renderered
        expect(screen.getByText(300)).toBeInTheDocument();
    });
  });

本文标签: javascriptReact component does not rerender under Jest on state changeStack Overflow