使用react-testing-library与Jest测试条件渲染失败问题求助
问题
测试React组件的条件渲染时,明明该通过的测试却失败了。组件代码如下:
import React, { useEffect, useState } from 'react'; export const Carousel = ({ skeleton }) => { const [isLoading, setIsLoading] = useState(skeleton !== undefined); useEffect(() => { setIsLoading(false); }, []) return ( <> <Skeleton data-testid="skeleton-wrapper" hidden={!isLoading} /> <CarouselItem data-testid="carousel-wrapper" hidden={isLoading} /> </> ) }
测试代码:
it('should switch display property', async () => { renderWithTheme( <Carousel skeleton={<div data-testid="skeleton-children">Skeleton children</div>}> <CarouselItem>Item 1</CarouselItem> </Carousel>, ); expect(screen.getByTestId('skeleton-children')).not.toHaveAttribute('hidden'); expect(screen.getByTestId('carousel-wrapper')).toHaveAttribute('hidden'); await waitFor(() => expect(screen.getByTestId('skeleton-wrapper')).toHaveAttribute('hidden')); expect(screen.getByTestId('carousel-wrapper')).toBeVisible(); });
测试失败表现:skeleton-wrapper已经处于隐藏状态,carousel-wrapper也已可见。但在useEffect里加1毫秒的setTimeout来执行setIsLoading(false)后,测试就能正常通过,求解惑。
分析与解决
问题根源
这是React渲染机制和测试执行顺序冲突导致的:
- 组件初始化时,
isLoading被设为true(因为传入了skeleton),首次渲染会显示骨架屏、隐藏轮播项。 useEffect的执行时机是首次渲染完成后同步执行,它会立刻把isLoading改成false,触发二次渲染。- 测试里的前两个断言是同步执行的,此时React已经完成了二次渲染,断言拿到的是更新后的状态,自然不符合预期。
而加了setTimeout后,setIsLoading(false)被放到了宏任务队列,会等当前所有同步代码(包括测试的前两个断言)执行完再执行,这时候测试就能捕获到初始的渲染状态,后续waitFor也能正确等待状态更新。
不用setTimeout的解决方法
方法1:调整测试断言方式
把前两个断言里的getByTestId换成queryByTestId——getBy会等待元素出现/状态稳定,可能等到了更新后的状态;而queryBy会直接返回当前DOM的状态,能捕获到初始渲染的结果:
it('should switch display property', async () => { renderWithTheme( <Carousel skeleton={<div data-testid="skeleton-children">Skeleton children</div>}> <CarouselItem>Item 1</CarouselItem> </Carousel>, ); // 用queryBy直接获取当前DOM状态 expect(screen.queryByTestId('skeleton-children')).not.toHaveAttribute('hidden'); expect(screen.queryByTestId('carousel-wrapper')).toHaveAttribute('hidden'); await waitFor(() => expect(screen.getByTestId('skeleton-wrapper')).toHaveAttribute('hidden')); expect(screen.getByTestId('carousel-wrapper')).toBeVisible(); });
方法2:调整组件状态更新时机
如果不想改测试,可以把useEffect里的状态更新改成微任务执行,这样既不用setTimeout,也能让测试的同步断言先执行:
useEffect(() => { // 用Promise.resolve把状态更新推到微任务队列 Promise.resolve().then(() => setIsLoading(false)); }, [])
总结
核心问题就是同步执行的useEffect瞬间覆盖了初始状态,导致测试的同步断言拿不到预期结果。加setTimeout只是绕开了这个问题,更合理的解决方式是调整测试断言逻辑,或者让状态更新延迟到微任务阶段。
内容的提问来源于stack exchange,提问作者Miriam
相关产品推荐
相关产品推荐

