测试
React Query 通过 Hook 实现功能——既可以使用我们提供的 Hook,也可以使用封装了这些 Hook 的自定义 Hook。
你可以使用 React Testing Library 编写这些自定义 Hook 的单元测试,它提供了 renderHook 并重新导出了 @testing-library/dom 等辅助函数,例如 waitFor。
通过运行以下命令安装:
1npm install @testing-library/react @testing-library/dom --save-dev我们的第一项测试
安装完成后,即可编写一个简单测试。假设有如下自定义 Hook:
1export function useCustomHook() {2 return useQuery({ queryKey: ['customHook'], queryFn: () => 'Hello' })3}我们可以为其编写如下测试:
1import { renderHook, waitFor } from '@testing-library/react'2 3const queryClient = new QueryClient()4const wrapper = ({ children }) => (5 <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>6)7 8const { result } = renderHook(() => useCustomHook(), { wrapper })9 10await waitFor(() => expect(result.current.isSuccess).toBe(true))11 12expect(result.current.data).toEqual('Hello')注意,我们提供了一个自定义包装器,用于构建 QueryClient 和 QueryClientProvider。这有助于确保我们的测试完全与其他测试隔离。
该包装器只需编写一次即可,但此时必须确保 QueryClient 在每次测试前都被清空,并且测试不能并行执行;否则一个测试会影响其他测试的结果。
关闭重试
该库默认启用三次带指数退避的重试,这意味着如果你希望测试一个出错的查询,测试很可能会超时。最简单的禁用重试方式是通过 QueryClientProvider。我们来扩展上面的示例:
1const queryClient = new QueryClient({2 defaultOptions: {3 queries: {4 // ✅ turns retries off5 retry: false,6 },7 },8})9const wrapper = ({ children }) => (10 <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>11)这会将组件树中所有查询的默认重试次数设为“零次”。需要注意的是,仅当实际的 useQuery 未显式设置 retries 选项时,此配置才生效;如果某个查询明确设置了 5 次重试,则该设置仍会优先生效,因为默认值仅作为回退方案。
使用 Jest 将 gcTime 设为 Infinity
如果你使用 Jest,可将 gcTime 设为 Infinity,以避免出现 “Jest 在测试运行完成一秒钟后仍未退出” 的错误提示。这是服务端的默认行为;仅当你显式设置了 gcTime 时,才需要手动配置此项。
测试网络请求
React Query 的主要用途是缓存网络请求,因此我们必须首先验证代码是否发出了正确的网络请求。
测试网络请求的方式有很多,本例中我们将使用 nock。
假设有如下自定义 Hook:
1function useFetchData() {2 return useQuery({3 queryKey: ['fetchData'],4 queryFn: () => request('/api/data'),5 })6}我们可以为其编写如下测试:
1const queryClient = new QueryClient()2const wrapper = ({ children }) => (3 <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>4)5 6const expectation = nock('http://example.com').get('/api/data').reply(200, {7 answer: 42,8})9 10const { result } = renderHook(() => useFetchData(), { wrapper })11 12await waitFor(() => expect(result.current.isSuccess).toBe(true))13 14expect(result.current.data).toEqual({ answer: 42 })此处我们利用 waitFor 并等待查询状态表明请求已成功完成。这样我们就能确认 Hook 已执行完毕,且应已获得正确的数据。
//: # 'NoteWaitFor1'
测试加载更多 / 无限滚动
首先,我们需要模拟 API 响应:
1function generateMockedResponse(page) {2 return {3 page: page,4 items: [...]5 }6}然后,我们的 nock 配置需根据页码区分响应,我们将借助 uri 实现这一点。
uri 的值在此处类似 /api/data?page=1 或 /api/data?page=2
1const expectation = nock('http://example.com')2 .persist()3 .get('/api/data')4 .query(true)5 .reply(200, (uri) => {6 const url = new URL(`http://example.com${uri}`)7 const { page } = Object.fromEntries(url.searchParams)8 return generateMockedResponse(Number(page))9 })(注意 .persist(),因为我们将会多次调用该端点)
现在我们可以安全地运行测试,关键在于等待数据断言通过:
1const { result } = renderHook(() => useInfiniteQueryCustomHook(), {2 wrapper,3})4 5await waitFor(() => expect(result.current.isSuccess).toBe(true))6 7expect(result.current.data.pages).toStrictEqual([generateMockedResponse(1)])8 9result.current.fetchNextPage()10 11await waitFor(() =>12 expect(result.current.data.pages).toStrictEqual([13 generateMockedResponse(1),14 generateMockedResponse(2),15 ]),16)17 18expectation.done()进一步阅读
如需更多技巧及一种基于 mock-service-worker 的替代配置方案,请参阅 TkDodo 撰写的《Testing React Query》一文。