测试
Svelte Query 通过函数(如 createQuery)来工作,这些函数可以是官方提供的,也可以是你自己封装的、在其基础上进行包装的自定义函数。它们会从 Svelte 上下文中读取 QueryClient(由 QueryClientProvider 设置),或从你作为最后一个参数传入的 QueryClient 访问器中读取。
遵循 Svelte 文档的测试指南,你可以在 $effect.root 中测试这些函数(位于 .svelte.test.ts 文件内),并使用 Svelte Testing Library 测试组件。
设置
运行以下命令安装 Vitest、jsdom 和 Svelte Testing Library:
1npm install vitest jsdom @testing-library/svelte @testing-library/jest-dom --save-dev然后将 svelteTesting 插件及用于 jest-dom 匹配器的设置文件添加到你的 Vite 配置中:
vite.config.ts
1import { svelte } from '@sveltejs/vite-plugin-svelte'2import { svelteTesting } from '@testing-library/svelte/vite'3import { defineConfig } from 'vitest/config'4 5export default defineConfig({6 plugins: [svelte(), svelteTesting()],7 test: {8 environment: 'jsdom',9 setupFiles: ['./vitest-setup.ts'],10 },11})vitest-setup.ts
1import '@testing-library/jest-dom/vitest'在 SvelteKit 项目中,请改用 sveltekit() 插件(来自 @sveltejs/kit/vite),而非 svelte()。
我们的第一项测试
假设有如下查询选项(与 queryOptions 共享):
queries.ts
1import { queryOptions } from '@tanstack/svelte-query'2 3export const greetingOptions = queryOptions({4 queryKey: ['greeting'],5 queryFn: () =>6 new Promise<string>((resolve) => setTimeout(() => resolve('Hello'), 10)),7})由于 Svelte 上下文仅存在于组件内部,因此需将一个全新的 QueryClient 作为第二个参数传给 createQuery。在 $effect.root 中创建查询,等待其成功完成,并在 finally 中清理 effect root,以确保即使断言失败也能完成清理:
queries.svelte.test.ts
1import { expect, it, vi } from 'vitest'2import { QueryClient, createQuery } from '@tanstack/svelte-query'3import { greetingOptions } from './queries'4import type { CreateQueryResult } from '@tanstack/svelte-query'5 6it('should fetch the greeting', async () => {7 const queryClient = new QueryClient()8 let query!: CreateQueryResult<string>9 const cleanup = $effect.root(() => {10 query = createQuery(11 () => greetingOptions,12 () => queryClient,13 )14 })15 16 try {17 await vi.waitFor(() => expect(query.isSuccess).toBe(true))18 19 expect(query.data).toBe('Hello')20 } finally {21 cleanup()22 }23})为避免在每个测试中重复编写 $effect.root 及其清理逻辑,你也可以用一个辅助函数将整个测试体包裹起来,使其在 effect root 内运行:
1import { expect, it, vi } from 'vitest'2import { QueryClient, createQuery } from '@tanstack/svelte-query'3import { greetingOptions } from './queries'4 5function withEffectRoot(fn: () => Promise<void>) {6 return async () => {7 let promise!: Promise<void>8 const cleanup = $effect.root(() => {9 promise = fn()10 })11 try {12 await promise13 } finally {14 cleanup()15 }16 }17}18 19it(20 'should fetch the greeting',21 withEffectRoot(async () => {22 const queryClient = new QueryClient()23 const query = createQuery(24 () => greetingOptions,25 () => queryClient,26 )27 28 await vi.waitFor(() => expect(query.isSuccess).toBe(true))29 30 expect(query.data).toBe('Hello')31 }),32)注意:我们为每个测试都创建了一个新的 QueryClient。这有助于确保测试完全与其他测试隔离。
你也可以在多个测试间共享单个 QueryClient,但此时必须确保每次测试前都清空该 QueryClient,且测试不能并行执行;否则一个测试会影响其他测试的结果。
query 是响应式的,因此应在等待之后再读取其属性,而不是在一开始就对其进行解构。解构得到的值会保持其被读取时的快照值。
测试组件
组件从 QueryClientProvider 所设置的上下文中读取 QueryClient。假设有一个使用相同查询选项的组件:
Greeting.svelte
1<script lang="ts">2 import { createQuery } from '@tanstack/svelte-query'3 import { greetingOptions } from './queries'4 5 const query = createQuery(() => greetingOptions)6</script>7 8{#if query.isPending}9 <p>Loading...</p>10{:else if query.isError}11 <p>Error: {query.error.message}</p>12{:else}13 <p>{query.data}</p>14{/if}在仅为测试而存在的组件中,用 QueryClientProvider 将其包裹:
GreetingTest.svelte
1<script lang="ts">2 import { QueryClientProvider } from '@tanstack/svelte-query'3 import type { QueryClient } from '@tanstack/svelte-query'4 import Greeting from './Greeting.svelte'5 6 let { queryClient }: { queryClient: QueryClient } = $props()7</script>8 9<QueryClientProvider client={queryClient}>10 <Greeting />11</QueryClientProvider>然后使用一个全新的 QueryClient 渲染它,并等待数据渲染完成:
Greeting.svelte.test.ts
1import { render } from '@testing-library/svelte'2import { expect, it } from 'vitest'3import { QueryClient } from '@tanstack/svelte-query'4import GreetingTest from './GreetingTest.svelte'5 6it('should render the greeting', async () => {7 const rendered = render(GreetingTest, {8 props: { queryClient: new QueryClient() },9 })10 11 expect(rendered.getByText('Loading...')).toBeInTheDocument()12 expect(await rendered.findByText('Hello')).toBeInTheDocument()13})模拟计时器
vi.waitFor 和 findByText 会持续重试直到断言通过,这对等待真实网络请求的查询函数有效。当查询函数依赖计时器时,你可以改用模拟计时器并主动推进它,例如借助 vi.advanceTimersByTimeAsync。请在 afterEach 中恢复真实计时器,以确保即使断言失败也能恢复:
1import { render } from '@testing-library/svelte'2import { afterEach, beforeEach, expect, it, vi } from 'vitest'3import { QueryClient } from '@tanstack/svelte-query'4import GreetingTest from './GreetingTest.svelte'5 6beforeEach(() => {7 vi.useFakeTimers()8})9 10afterEach(() => {11 vi.useRealTimers()12})13 14it('should render the greeting', async () => {15 const rendered = render(GreetingTest, {16 props: { queryClient: new QueryClient() },17 })18 19 expect(rendered.getByText('Loading...')).toBeInTheDocument()20 21 await vi.advanceTimersByTimeAsync(10)22 23 expect(rendered.getByText('Hello')).toBeInTheDocument()24})关闭重试
该库默认启用三次指数退避重试,这意味着如果你希望测试一个出错的查询,测试很可能会超时。最简单的关闭重试方式是通过你在测试中使用的 QueryClient:
1const queryClient = new QueryClient({2 defaultOptions: {3 queries: {4 // ✅ turns retries off5 retry: false,6 },7 },8})这会将所有使用该客户端的查询的默认重试策略设为“不重试”。需要注意的是,该设置仅在你实际的 createQuery 未显式指定重试次数时才生效。例如,若某个查询明确设置了 5 次重试,则该设置仍会优先于默认值,因为默认值仅作为后备方案使用。