框架
版本

测试

React Query 通过 hooks 工作 - 无论是我们提供的 hooks 还是围绕它们构建的自定义 hooks。

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

通过运行以下命令安装它

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

我们的第一个测试

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

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')

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

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

关闭重试

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

tsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // ✅ turns retries off
      retry: false,
    },
  },
})
const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // ✅ turns retries off
      retry: false,
    },
  },
})
const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)

这将将组件树中所有 query 的默认值设置为“不重试”。重要的是要知道,这仅在您实际的 useQuery 没有设置显式重试的情况下才有效。如果您有一个想要 5 次重试的 query,这仍然优先,因为默认值仅作为后备。

使用 Jest 将 gcTime 设置为 Infinity

如果您使用 Jest,您可以将 gcTime 设置为 Infinity 以防止出现“Jest did not exit one second after the test run completed”错误消息。这是服务器上的默认行为,仅在您显式设置 gcTime 时才需要设置。

测试网络调用

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

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

给定以下自定义 hook

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 并等待直到 query 状态指示请求已成功。这样我们就知道我们的 hook 已经完成并且应该有正确的数据。注意:当使用 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