You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 03:24:58