组件:
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;
};
Jest测试:
it('should ...', async () => {
render(
<MyComponent price={300}/>)
)
await waitFor(() => {
expect(...).toBeInTheDocument();
});
});
上面的代码确实发生了MyComponent
,在运行测试时,只渲染一次,而不是两次(当应用程序运行时)。在result1
是空数组的初始渲染之后,useResult
的useEffect
正在运行,并且由于setResult(data)
导致状态发生变化,因此我应该期望MyComponent
被重新渲染。然而,事实并非如此,result1
仍然等于[]
,而它应该等于[{price:300}]
。
因此,测试中的自定义挂钩的行为似乎与实际应用程序不同。我认为可以通过调用它们的组件来间接测试它们。
对上述有任何解释/想法吗?
更新
引发上述错误行为的问题是状态突变!!它适用于应用程序,但不适用于测试!我的错误是试图使用push
向作为状态变量的数组中添加元素
好吧,您似乎在问一个关于测试自定义钩子的非常具体的问题。在这种情况下,我在过去通过@testing-library
测试自定义挂钩时也遇到了一些问题,并且创建了一个不同的包(最近合并到@testing-library
中),该包提供了用于测试自定义挂钩的renderHook()
功能。我建议你测试一下。
- 原始包(不要使用它。直接使用TL包)
- TL文档中有关
renderHook()
调用的文档
你可以在Kent C.Dods的这篇博客文章中阅读更多关于它的信息。
我还建议您创建一个";状态改变";以测试您的组件并使用renderHook()
测试挂钩。
这里有一个简单的代码沙盒,其中包含一些与您的案例类似的组件测试。
原始答案
从本质上讲,您的测试并不是在等待组件执行副作用。有两种方法可以等待:
- 使用
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(在此处读取文档),并且是waitFor
和getBy*
查询的组合(在此处阅读文档)
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();
});
步骤1:正在测试的代码
如果如问题评论中所述,效果内部的操作是同步的,则在所有情况下都不希望使用useEffect
基于道具设置此状态。不仅仅是为了测试。
该组件将渲染、更新DOM,并立即需要重新渲染以下帧,因为它的状态已更新。它会给用户带来闪光效果,并不必要地降低应用程序的速度。
如果操作很便宜,那么只在每次渲染时执行它会更高效。
如果操作可能更昂贵,您可以将其封装在useMemo
中,以确保它只在输入发生更改时发生。
export const useResult = (price) => {
return useMemo(
// I assume this is a stub for a expensive operation.
() => [{price: price}],
[price]
);
};
如果出于某种不明确的原因,您确实需要在效果上这样做(您可能不需要,但存在边缘情况),则可以使用layoutEffect
。它将被同步处理,并避免闪烁的帧仍然不建议使用,但与常规效果相比略有改善。
第2步:测试
如果您将组件更改为不使用效果,那么它从第一次渲染开始就应该是正确的,并且您不再有问题了。从一开始就避免出现问题也是一个有效的解决方案:D
如果您确实发现需要在测试中同步刷新某些内容,那么现在有flushSync
函数可以做到这一点。
也许它也会刷新状态更新,使您的测试在没有其他更改的情况下工作。我想应该是这样,因为刷新时效果触发的新更新应该在返回之前继续处理。
flushSync(() => {
render(
<MyComponent price={300}/>)
)
})
在任何情况下,如果可以改进组件以修复通过在效果中设置状态而引入的附加渲染,那么这样做都没有意义。
你可以做:
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();
})
您没有等待组件被重新提交
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();
});
});