状态更改时,React组件不会在Jest下重新呈现

iswrvxsc  于 2022-12-08  发布在  Jest
关注(0)|答案(4)|浏览(224)

组成部分:

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

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

自定义挂钩:

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

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

  return result;
};

玩笑测试:

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

上面的代码所发生的情况是MyComponent在运行测试时,只呈现一次而不是两次(当应用程序运行时)。在result1为空数组的初始渲染之后,useResultuseEffect正在运行,并且由于存在由于setResult(data)而导致的状态更改,因此我应该期望重新渲染MyComponent。然而,事实并非如此,result1仍然等于[],而它应该等于[{price:300}].
因此,* 在测试中的自定义钩子的行为似乎与真实的的应用程序不同 *。我认为通过调用它们的组件间接测试它们是可以的。

***对上述***有何解释/想法?
更新

  • 调用上述错误行为的问题是状态突变!!它在应用程序中工作,但在测试中不工作!我的错误是试图使用push向状态变量数组中添加元素... *
8hhllhi2

8hhllhi21#

好吧,看起来你问的是一个关于测试自定义钩子的非常具体的问题。在这种情况下,我在过去通过@testing-library测试自定义钩子时也遇到了一些问题,并且创建了一个不同的包(最近被合并到@testing-library中),它提供了renderHook()函数来测试自定义钩子。我建议你测试它。

  • Original Package(不使用,直接使用TL)
  • TL文档中关于renderHook()调用的文档

您可以在Kent C. Dodds的这篇博客文章中阅读更多关于它的信息。
我还建议您创建一个“状态更改”来测试组件,并使用renderHook()测试钩子。
此处is a simple codesandbox with some tests表示与您的情况类似的组件。

原始答案

从本质上讲,你的测试不是等待组件执行副作用,有两种等待的方式:

  • 使用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()
    )
  });
  • 使用RTL中的findBy*查询,该查询返回Promise(在此处阅读文档),并且是waitForgetBy*查询的组合(在此处阅读文档)
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();
  });
gdrx4gfi

gdrx4gfi2#

Step 1: the code being tested

If, as mentioned in the comments 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 component 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 recommend it but it's a slight improvement over a regular effect.

Step 2: Testing

If you changed the component 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 component to fix the additional render introduced by setting state in an effect.

0vvn1miw

0vvn1miw3#

您可以:

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();
})
6mw9ycah

6mw9ycah4#

您没有等待组件重新呈现

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

相关问题