框架
版本

测试

React Query 通过钩子(我们提供的钩子或围绕它们包装的自定义钩子)工作。

对于 React 17 或更早版本,可以使用 React Hooks Testing Library 库为这些自定义钩子编写单元测试。

通过运行以下命令安装此库:

sh
npm install @testing-library/react-hooks react-test-renderer --save-dev
npm install @testing-library/react-hooks react-test-renderer --save-dev

react-test-renderer 库作为 @testing-library/react-hooks 的对等依赖项是必需的,并且需要与您正在使用的 React 版本相对应。)

注意:使用 React 18 或更高版本时,renderHook 可直接通过 @testing-library/react 包获得,不再需要 @testing-library/react-hooks

我们的第一个测试

安装完成后,可以编写一个简单的测试。给定以下自定义钩子:

tsx
export function useCustomHook() {
  return useQuery({ queryKey: ['customHook'], queryFn: () => 'Hello' })
}
export function useCustomHook() {
  return useQuery({ queryKey: ['customHook'], queryFn: () => 'Hello' })
}

我们可以为此编写如下测试:

tsx
import { renderHook, waitFor } from '@testing-library/react'

const queryClient = new QueryClient()
const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)

const { result } = renderHook(() => useCustomHook(), { wrapper })

await waitFor(() => expect(result.current.isSuccess).toBe(true))

expect(result.current.data).toEqual('Hello')
import { renderHook, waitFor } from '@testing-library/react'

const queryClient = new QueryClient()
const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)

const { result } = renderHook(() => useCustomHook(), { wrapper })

await waitFor(() => expect(result.current.isSuccess).toBe(true))

expect(result.current.data).toEqual('Hello')

请注意,我们提供了一个自定义包装器,用于构建 QueryClientQueryClientProvider。这有助于确保我们的测试与任何其他测试完全隔离。

可以只编写一次此包装器,但如果这样做,我们需要确保在每次测试之前清除 QueryClient,并且测试不会并行运行,否则一个测试会影响其他测试的结果。

关闭重试

该库默认为三次重试,并采用指数退避算法,这意味着如果您想测试错误的查询,您的测试很可能会超时。关闭重试的最简单方法是通过 QueryClientProvider。让我们扩展上面的示例:

tsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // ✅ 关闭重试
      retry: false,
    },
  },
})
const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // ✅ 关闭重试
      retry: false,
    },
  },
})
const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)

这将为组件树中的所有查询设置默认值为“不重试”。重要的是要知道,只有当您的实际 useQuery 没有设置显式重试时,这才有效。如果您的查询需要 5 次重试,则仍将优先考虑此设置,因为默认值仅作为后备。

使用 Jest 将 gcTime 设置为 Infinity

如果您使用 Jest,可以将 gcTime 设置为 Infinity,以防止出现“测试运行完成后一秒 Jest 未退出”的错误消息。这是服务器上的默认行为,仅在您显式设置 gcTime 时才需要设置。

测试网络调用

React Query 的主要用途是缓存网络请求,因此测试我们的代码是否首先发出正确的网络请求非常重要。

有很多方法可以测试这些,但在此示例中,我们将使用 nock

给定以下自定义钩子:

tsx
function useFetchData() {
  return useQuery({
    queryKey: ['fetchData'],
    queryFn: () => request('/api/data'),
  })
}
function useFetchData() {
  return useQuery({
    queryKey: ['fetchData'],
    queryFn: () => request('/api/data'),
  })
}

我们可以为此编写如下测试:

tsx
const queryClient = new QueryClient()
const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)

const expectation = nock('http://example.com').get('/api/data').reply(200, {
  answer: 42,
})

const { result } = renderHook(() => useFetchData(), { wrapper })

await waitFor(() => expect(result.current.isSuccess).toBe(true))

expect(result.current.data).toEqual({ answer: 42 })
const queryClient = new QueryClient()
const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)

const expectation = nock('http://example.com').get('/api/data').reply(200, {
  answer: 42,
})

const { result } = renderHook(() => useFetchData(), { wrapper })

await waitFor(() => expect(result.current.isSuccess).toBe(true))

expect(result.current.data).toEqual({ answer: 42 })

在这里,我们正在使用 waitFor 并等待查询状态指示请求已成功。这样我们就知道我们的钩子已经完成并且应该具有正确的数据。注意:使用 React 18 时,waitFor 的语义已如上所述发生更改。

测试加载更多/无限滚动

首先我们需要模拟我们的 API 响应

tsx
function generateMockedResponse(page) {
  return {
    page: page,
    items: [...]
  }
}
function generateMockedResponse(page) {
  return {
    page: page,
    items: [...]
  }
}

然后,我们的 nock 配置需要根据页面区分响应,我们将使用 uri 来执行此操作。 此处 uri 的值将类似于 "/?page=1/?page=2

tsx
const expectation = nock('http://example.com')
  .persist()
  .query(true)
  .get('/api/data')
  .reply(200, (uri) => {
    const url = new URL(`http://example.com${uri}`)
    const { page } = Object.fromEntries(url.searchParams)
    return generateMockedResponse(page)
  })
const expectation = nock('http://example.com')
  .persist()
  .query(true)
  .get('/api/data')
  .reply(200, (uri) => {
    const url = new URL(`http://example.com${uri}`)
    const { page } = Object.fromEntries(url.searchParams)
    return generateMockedResponse(page)
  })

(注意 .persist(),因为我们将多次从此端点调用)

现在我们可以安全地运行我们的测试了,这里的技巧是等待数据断言通过:

tsx
const { result } = renderHook(() => useInfiniteQueryCustomHook(), {
  wrapper,
})

await waitFor(() => expect(result.current.isSuccess).toBe(true))

expect(result.current.data.pages).toStrictEqual(generateMockedResponse(1))

result.current.fetchNextPage()

await waitFor(() =>
  expect(result.current.data.pages).toStrictEqual([
    ...generateMockedResponse(1),
    ...generateMockedResponse(2),
  ]),
)

expectation.done()
const { result } = renderHook(() => useInfiniteQueryCustomHook(), {
  wrapper,
})

await waitFor(() => expect(result.current.isSuccess).toBe(true))

expect(result.current.data.pages).toStrictEqual(generateMockedResponse(1))

result.current.fetchNextPage()

await waitFor(() =>
  expect(result.current.data.pages).toStrictEqual([
    ...generateMockedResponse(1),
    ...generateMockedResponse(2),
  ]),
)

expectation.done()

注意:使用 React 18 时,waitFor 的语义已如上所述发生更改。

进一步阅读

有关其他提示和使用 mock-service-worker 的替代设置,请参阅社区资源中的测试 React Query