如何解决使用Tanstack Query的组件Testing Library测试超时问题?
如何为使用Tanstack Query的组件编写Testing Library测试?
我在测试使用Tanstack Query的组件时遇到了超时问题,isLoading始终为true,data一直是undefined。最初是在MSW + Testing Library + Tanstack Query的组合场景下发现这个问题的。
测试代码
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { screen, render, waitFor } from '@testing-library/react' import { PropsWithChildren } from 'react' import { expect, describe, it } from 'vitest' import { Example } from './Example' const queryClient = new QueryClient({ defaultOptions: { queries: { cacheTime: 0, staleTime: 0, retry: false, }, }, }) const AllTheProviders = ({ children }: PropsWithChildren) => { return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ) } describe('Example', () => { it('Load Complete', async () => { render(<Example scenario="success" />, { wrapper: AllTheProviders }) expect(screen.queryByText('Loading')).toBeInTheDocument() await waitFor(() => { expect(screen.queryByText('Loading')).not.toBeInTheDocument() expect(screen.queryByText('todo-1')).toBeInTheDocument() }) }) it('Is Loading', async () => { render(<Example scenario="success" />, { wrapper: AllTheProviders }) await waitFor(() => { expect(screen.queryByLabelText('loading')).toBeInTheDocument() expect(screen.queryByLabelText('todo-1')).not.toBeInTheDocument() }) }) })
组件代码
import { useQuery } from '@tanstack/react-query' const awaitTimeout = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay)) export const Example = ({ scenario }: { scenario: 'loading' | 'success' }) => { const query = useQuery({ queryKey: ['example', scenario], queryFn: async (data) => { if (data.queryKey.includes('loading')) { return awaitTimeout(1000) } await awaitTimeout(200) return { userId: 1, id: 1, title: 'todo-1', completed: false, } }, }) if (query.isLoading) { return <div>Loading</div> } if (query.isError) { return <div aria-label="error">Error</div> } if (query.isSuccess && query.data) { return <div aria-label="todo-1">{query.data?.title}</div> } return <div aria-label="unknown">Unknown Issue</div> }
测试结果
执行命令:vitest --run Example.test.tsx
Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". at Timeout.<anonymous> (file:///Users/johnbailey/Development/Tryyb/spacedock/node_modules/.pnpm/@vitest+runner@0.34.6/node_modules/@vitest/runner/dist/index.js:62:16) at listOnTimeout (node:internal/timers:564:17) at processTimers (node:internal/timers:507:7)
问题分析与解决方法
1. 核心问题
- 真实定时器导致测试超时:组件里用
setTimeout模拟异步请求,真实延迟会让测试等待过久触发超时。 - 共用QueryClient导致缓存污染:所有测试复用同一个
QueryClient,第一个测试的查询结果会被缓存,干扰后续测试的状态判断。 - 测试逻辑错误:第二个测试要验证加载状态,但传入的是
success场景,且断言的loading标签不存在于组件中。
2. 修正后的测试代码
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { screen, render, waitFor } from '@testing-library/react' import { PropsWithChildren } from 'react' import { expect, describe, it, vi, beforeEach } from 'vitest' import { Example } from './Example' // 每个测试前初始化新的QueryClient,避免缓存污染 let queryClient: QueryClient beforeEach(() => { queryClient = new QueryClient({ defaultOptions: { queries: { cacheTime: 0, staleTime: 0, retry: false, }, }, }) // 启用假定时器,跳过真实延迟 vi.useFakeTimers() }) // 测试后恢复真实定时器 afterEach(() => { vi.useRealTimers() }) const AllTheProviders = ({ children }: PropsWithChildren) => { return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ) } describe('Example', () => { it('加载完成后显示数据', async () => { render(<Example scenario="success" />, { wrapper: AllTheProviders }) // 初始状态显示加载文字 expect(screen.getByText('Loading')).toBeInTheDocument() // 快进时间到异步操作完成 vi.advanceTimersByTime(200) await waitFor(() => { expect(screen.queryByText('Loading')).not.toBeInTheDocument() expect(screen.getByLabelText('todo-1')).toBeInTheDocument() }) }) it('加载过程中显示Loading状态', async () => { render(<Example scenario="loading" />, { wrapper: AllTheProviders }) // 异步未完成时,Loading元素存在,数据元素不存在 expect(screen.getByText('Loading')).toBeInTheDocument() expect(screen.queryByLabelText('todo-1')).not.toBeInTheDocument() // 可选:快进时间后验证加载状态消失 vi.advanceTimersByTime(1000) await waitFor(() => { expect(screen.queryByText('Loading')).not.toBeInTheDocument() }) }) })
3. 关键优化点
- 每个测试独立创建QueryClient:通过
beforeEach在每个测试前初始化新客户端,彻底避免跨测试的缓存干扰。 - 使用假定时器加速测试:用Vitest的
vi.useFakeTimers()模拟定时器,vi.advanceTimersByTime()直接快进到异步操作完成的时间,解决超时问题。 - 修正测试逻辑:第二个测试改为传入
loading场景,并且使用正确的断言元素(组件中没有aria-label="loading",直接断言Loading文本)。 - 使用更精确的查询方法:
getByText会自动等待元素出现(默认1000ms超时),适合断言存在的元素;queryBy用于断言元素不存在。
4. Tanstack Query测试最佳实践
- 用
@tanstack/react-query/testing提供的工具(如renderHook、waitForQueryData)简化查询测试。 - 配合MSW模拟API请求,避免依赖真实后端,让测试更稳定。
- 测试时禁用查询重试、设置短缓存时间,确保测试结果可预测。
内容的提问来源于stack exchange,提问作者John Timothy Bailey
相关产品推荐
相关产品推荐

