测试
Lit Query 通过响应式控制器(例如 createQueryController)实现功能,这些控制器附加到宿主元素上。控制器会从最近已连接的 QueryClientProvider 中读取 QueryClient,或从你作为其最后一个参数传入的 QueryClient 中读取,并在其宿主连接到文档时进行订阅。
因此,要测试一个查询,需将宿主元素连接到文档,等待元素更新,然后断言其渲染结果或控制器的返回值。
环境配置
本指南使用 Vitest 配合 jsdom(与 Lit Query 自身的测试方式一致),以及 jest-dom 匹配器。运行以下命令安装它们:
1npm install vitest jsdom @testing-library/jest-dom --save-dev然后启用 jsdom,并添加一个配置文件,在其中加载匹配器并一次性注册 QueryClientProvider:
vite.config.ts
1import { defineConfig } from 'vitest/config'2 3export default defineConfig({4 test: {5 environment: 'jsdom',6 setupFiles: ['./vitest-setup.ts'],7 },8})vitest-setup.ts
1import '@testing-library/jest-dom/vitest'2import { QueryClientProvider } from '@tanstack/lit-query'3 4customElements.define('query-client-provider', QueryClientProvider)我们的第一项测试
假设有如下查询选项(与 queryOptions 共享),以及一个使用这些选项的元素:
queries.ts
1import { queryOptions } from '@tanstack/lit-query'2 3export const greetingOptions = queryOptions({4 queryKey: ['greeting'],5 queryFn: () =>6 new Promise<string>((resolve) => setTimeout(() => resolve('Hello'), 10)),7})greeting-view.ts
1import { LitElement, html } from 'lit'2import { createQueryController } from '@tanstack/lit-query'3import { greetingOptions } from './queries'4 5export class GreetingView extends LitElement {6 private readonly greeting = createQueryController(this, greetingOptions)7 8 render() {9 const query = this.greeting()10 11 if (query.isPending) return html`<p>Loading...</p>`12 if (query.isError) return html`<p>Error: ${query.error.message}</p>`13 14 return html`<p>${query.data}</p>`15 }16}17 18customElements.define('greeting-view', GreetingView)我们可在 QueryClientProvider 下渲染该元素,并传入一个全新的 QueryClient;等待其首次更新,再等待数据完成渲染:
greeting-view.test.ts
1import { afterEach, expect, it, vi } from 'vitest'2import { QueryClient } from '@tanstack/lit-query'3import type { QueryClientProvider } from '@tanstack/lit-query'4import type { GreetingView } from './greeting-view'5import './greeting-view'6 7afterEach(() => {8 document.body.replaceChildren()9})10 11it('should render the greeting', async () => {12 const provider = document.createElement(13 'query-client-provider',14 ) as QueryClientProvider15 provider.client = new QueryClient()16 const view = document.createElement('greeting-view') as GreetingView17 provider.append(view)18 document.body.append(provider)19 20 await view.updateComplete21 expect(view.shadowRoot).toHaveTextContent('Loading...')22 23 await vi.waitFor(() => expect(view.shadowRoot).toHaveTextContent('Hello'))24})在 afterEach 中移除元素会使它们断开连接,从而导致其控制器从 QueryClient 取消订阅。
注意,我们为每个测试都创建了一个新的 QueryClient。这有助于确保测试完全与其他测试隔离。
你也可以在多个测试间共享单个 QueryClient,但此时必须确保每次测试前都清空该 QueryClient,且测试不能并行执行;否则一个测试会影响其他测试的结果。
单独测试控制器
若要在不渲染的情况下测试查询控制器,可将控制器附加到仅为测试而存在的宿主元素上,并将 QueryClient 作为其最后一个参数传入(而非渲染一个提供者)。然后对控制器的返回值进行断言:
1import { afterEach, expect, it, vi } from 'vitest'2import { LitElement } from 'lit'3import { QueryClient, createQueryController } from '@tanstack/lit-query'4import { greetingOptions } from './queries'5 6afterEach(() => {7 document.body.replaceChildren()8})9 10it('should fetch the greeting', async () => {11 const queryClient = new QueryClient()12 13 class TestHost extends LitElement {14 readonly greeting = createQueryController(15 this,16 greetingOptions,17 queryClient,18 )19 }20 customElements.define('test-host', TestHost)21 22 const host = new TestHost()23 document.body.append(host)24 25 await vi.waitFor(() => expect(host.greeting().isSuccess).toBe(true))26 27 expect(host.greeting().data).toBe('Hello')28})自定义元素名称只能注册一次,因此请为每个测试宿主指定唯一名称。
模拟计时器
vi.waitFor 会持续重试直到断言通过,这对等待真实请求的查询函数有效。当查询函数依赖计时器时,你可以改用模拟计时器并手动推进时间,例如借助 vi.advanceTimersByTimeAsync。请在 afterEach 中恢复真实计时器,以确保即使断言失败也能正确恢复:
1import { afterEach, beforeEach, expect, it, vi } from 'vitest'2import { QueryClient } from '@tanstack/lit-query'3import type { QueryClientProvider } from '@tanstack/lit-query'4import type { GreetingView } from './greeting-view'5import './greeting-view'6 7beforeEach(() => {8 vi.useFakeTimers()9})10 11afterEach(() => {12 document.body.replaceChildren()13 vi.useRealTimers()14})15 16it('should render the greeting', async () => {17 const provider = document.createElement(18 'query-client-provider',19 ) as QueryClientProvider20 provider.client = new QueryClient()21 const view = document.createElement('greeting-view') as GreetingView22 provider.append(view)23 document.body.append(provider)24 25 await view.updateComplete26 expect(view.shadowRoot).toHaveTextContent('Loading...')27 28 await vi.advanceTimersByTimeAsync(10)29 30 expect(view.shadowRoot).toHaveTextContent('Hello')31})关闭重试
该库默认启用三次指数退避重试,这意味着如果你希望测试一个出错的查询,测试很可能会超时。最简单的关闭重试方式是通过你在测试中使用的 QueryClient 实现:
1const queryClient = new QueryClient({2 defaultOptions: {3 queries: {4 // ✅ turns retries off5 retry: false,6 },7 },8})这会将所有使用该客户端的查询的默认重试策略设为“不重试”。需要注意的是,此设置仅在你的实际 createQueryController 未显式指定重试次数时才生效;如果某个查询明确设置了 5 次重试,该设置仍将优先于默认值,因为默认值仅作为后备方案使用。