测试
Preact Query 通过 Hook 实现功能——既可以使用我们提供的 Hook,也可以使用封装了这些 Hook 的自定义 Hook。
你可以使用 Preact Testing Library 来为这些自定义 Hook 编写单元测试,它提供了 renderHook 并重新导出了 @testing-library/dom 等辅助函数,例如 waitFor。
通过运行以下命令安装:
1npm install @testing-library/preact --save-dev我们的第一项测试
安装完成后,即可编写一个简单测试。假设有如下自定义 Hook:
1export function useCustomHook() {2 return useQuery({ queryKey: ['customHook'], queryFn: () => 'Hello' })3}我们可以为其编写如下测试:
1import { renderHook, waitFor } from '@testing-library/preact'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 在每次测试前都被清空,并且测试不能并行执行;否则一个测试会影响其他测试的结果。
关闭重试机制
该库默认启用三次指数退避重试(retry),这意味着如果你希望测试一个出错的查询,测试很可能会超时。最简单的禁用重试方式是通过 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 did not exit one second after the test run completed” 错误提示。这是服务端的默认行为;仅当你显式设置了 gcTime 时,才需要手动配置此项。
测试网络请求
Preact 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 已执行完毕,并应持有正确的数据。
测试加载更多 / 无限滚动
首先我们需要模拟 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 关于测试的文章。该文面向另一种框架适配器撰写,但其中方法同样适用于此处。