React Query 通过 hooks 实现,无论是我们提供的 hooks 还是封装了它们的自定义 hooks。
使用 React 17 或更早版本时,可以通过 React Hooks Testing Library 库为这些自定义 hooks 编写单元测试。
通过运行以下命令安装:
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:
export function useCustomHook() {
return useQuery({ queryKey: ['customHook'], queryFn: () => 'Hello' })
}
export function useCustomHook() {
return useQuery({ queryKey: ['customHook'], queryFn: () => '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')
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,它构建了 QueryClient 和 QueryClientProvider。这有助于确保我们的测试与任何其他测试完全隔离。
可以只编写一次这个 wrapper,但如果这样做,我们需要确保在每次测试前都清空 QueryClient,并且不要并行运行测试,否则一个测试会影响其他测试的结果。
该库默认提供三次带指数退避的重试,这意味着如果您想测试一个有错误查询,您的测试很可能会超时。最简单的关闭重试的方法是通过 QueryClientProvider。让我们扩展上面的示例:
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>
)
这将为组件树中的所有查询设置“无重试”的默认值。重要的是要知道,只有当您的实际 `useQuery` 没有显式设置重试时,这才会起作用。如果您有一个需要 5 次重试的查询,它仍然会优先,因为默认值只作为回退。
如果您使用 Jest,可以将 gcTime 设置为 Infinity,以防止出现“Jest 在测试运行完成后一秒钟未退出”的错误消息。这是服务器上的默认行为,只有在您显式设置了 gcTime 时才有必要设置。
React Query 的主要用途是缓存网络请求,因此,首先能够测试我们的代码是否正确地进行了网络请求非常重要。
有许多方法可以测试这些,但在这个示例中,我们将使用 nock。
给定以下自定义 hook:
function useFetchData() {
return useQuery({
queryKey: ['fetchData'],
queryFn: () => request('/api/data'),
})
}
function useFetchData() {
return useQuery({
queryKey: ['fetchData'],
queryFn: () => request('/api/data'),
})
}
我们可以这样编写测试:
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,并等待查询状态指示请求已成功。这样我们就知道我们的 hook 已经完成并且应该具有正确的数据。注意:使用 React 18 时,waitFor 的语义已如上所述发生变化。
首先,我们需要模拟我们的 API 响应:
function generateMockedResponse(page) {
return {
page: page,
items: [...]
}
}
function generateMockedResponse(page) {
return {
page: page,
items: [...]
}
}
然后,我们需要通过 uri 来区分来自不同页面的响应。这里的 uri 值将类似于 "/?page=1 或 /?page=2。
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(),因为我们将多次从此端点调用)
现在我们可以安全地运行我们的测试了,这里的技巧是等待数据断言通过。
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 的替代设置,请参阅社区资源中的 Testing React Query。