译零 TransZero · TanStack Query 中文文档
主站

测试

更新于 2026-11-02 Angular 查看英文原文 ↗

大多数使用 TanStack Query 的 Angular 测试会涉及调用 injectQuery/injectMutation 的服务或组件。

TanStack Query 的 inject* 函数与 PendingTasks 集成,确保框架知晓正在进行的查询和修改。

这使得框架(例如在服务端渲染期间)能够等待修改和查询完成后再继续。在单元测试中,ApplicationRef.whenStable() 和 fixture.whenStable() 在工作立即结束时(例如同步查询函数或通过 HttpTestingController 刷新响应)对 Zone.js 和无 Zone 设置均有效。但它们可能在耗时的查询或修改完成前就已解析,因此应改用推进模拟计时器的方式,如下文各节所示。

此集成要求 Angular 19 或更高版本。早期版本的 Angular 不支持 PendingTasks。

TestBed 设置

为每个测试用例创建一个全新的 QueryClient,并使用 provideTanStackQuery 或 provideQueryClient 提供它。这可使缓存相互隔离,并允许你为每个测试单独更改默认选项:

TypeScript
1const queryClient = new QueryClient({2  defaultOptions: {3    queries: {4      retry: false, // ✅ faster failure tests5    },6  },7})8 9TestBed.configureTestingModule({10  providers: [provideTanStackQuery(queryClient)],11})

如果你的应用程序实际使用的 TanStack Query 配置被用于单元测试,请确保 withDevtools 不会意外包含在测试提供者中。这可能导致测试变慢。最好将测试配置与生产配置分开。

如果你共享辅助函数,请记得在 afterEach 中调用 queryClient.clear()(或构建新实例),以确保一个测试的数据不会泄漏到另一个测试中。

首个查询测试

查询测试通常在 TestBed.runInInjectionContext 内运行,然后等待稳定性:

TypeScript
1const appRef = TestBed.inject(ApplicationRef)2const query = TestBed.runInInjectionContext(() =>3  injectQuery(() => ({4    queryKey: ['greeting'],5    queryFn: () => 'Hello',6  })),7)8 9TestBed.tick() // Trigger effect10 11// Application is stable when queries are idle12await appRef.whenStable()13 14expect(query.status()).toBe('success')15expect(query.data()).toBe('Hello')

此处 whenStable() 在查询完成后解析,因为查询函数立即返回。当使用模拟计时器(Vitest)时,在等待稳定性前需推进计时器和微任务:

TypeScript
1await vi.advanceTimersByTimeAsync(0)2await Promise.resolve()3await appRef.whenStable()

组件测试

对于组件,请通过 TestBed.createComponent 创建它们。fixture.whenStable() 可能在查询数据到达前即已解析;因此在启用模拟计时器时,应先将计时器推进至超过查询函数的延迟,再运行 fixture.detectChanges() 并断言组件的渲染结果:

angular-ts
1beforeEach(() => {2  vi.useFakeTimers()3})4 5afterEach(() => {6  vi.useRealTimers()7})8 9it('should render the query data', async () => {10  @Component({11    template: `<p>{{ query.data()?.value ?? 'Loading...' }}</p>`,12  })13  class ExampleComponent {14    readonly query = injectQuery(() => ({15      queryKey: ['example'],16      queryFn: () =>17        new Promise<{ value: number }>((resolve) =>18          setTimeout(() => resolve({ value: 42 }), 10),19        ),20    }))21  }22 23  const fixture = TestBed.createComponent(ExampleComponent)24  fixture.detectChanges()25  expect(fixture.nativeElement.textContent).toContain('Loading...')26 27  await vi.advanceTimersByTimeAsync(11)28  fixture.detectChanges()29 30  expect(fixture.nativeElement.textContent).toContain('42')31})

如果你使用 Angular Testing Library,请使用 render 渲染组件,并遵循相同流程,通过 rendered.fixture 运行变更检测:

angular-ts
1it('should render the query data', async () => {2  @Component({3    template: `<p>{{ query.data()?.value ?? 'Loading...' }}</p>`,4  })5  class ExampleComponent {6    readonly query = injectQuery(() => ({7      queryKey: ['example'],8      queryFn: () =>9        new Promise<{ value: number }>((resolve) =>10          setTimeout(() => resolve({ value: 42 }), 10),11        ),12    }))13  }14 15  const rendered = await render(ExampleComponent)16  expect(rendered.getByText('Loading...')).toBeInTheDocument()17 18  await vi.advanceTimersByTimeAsync(11)19  rendered.fixture.detectChanges()20 21  expect(rendered.getByText('42')).toBeInTheDocument()22})

若要在不依赖组件的情况下测试同一查询,请使用 TestBed.runInInjectionContext 创建它,以相同方式推进计时器,并对其信号进行断言:

TypeScript
1it('should resolve the query data', async () => {2  const query = TestBed.runInInjectionContext(() =>3    injectQuery(() => ({4      queryKey: ['example'],5      queryFn: () =>6        new Promise<{ value: number }>((resolve) =>7          setTimeout(() => resolve({ value: 42 }), 10),8        ),9    })),10  )11  expect(query.isPending()).toBe(true)12 13  await vi.advanceTimersByTimeAsync(11)14 15  expect(query.data()).toEqual({ value: 42 })16})

处理重试

重试会使失败的测试变慢,因为默认退避策略会执行三次。可通过 defaultOptions 或按查询设置 retry: false(或指定具体次数)来加快测试速度。如果查询有意重试,请断言最终状态而非中间状态次数。

HttpClient 与网络桩(stub)

Angular 的 HttpClient 测试工具与 PendingTasks 协作良好。请将 provideHttpClient() 和 provideHttpClientTesting() 与 Query 提供者一同注册,并通过 HttpTestingController 刷新响应:

TypeScript
1TestBed.configureTestingModule({2  providers: [3    provideHttpClient(),4    provideHttpClientTesting(),5    provideTanStackQuery(queryClient),6  ],7})8 9const httpCtrl = TestBed.inject(HttpTestingController)10const query = TestBed.runInInjectionContext(() =>11  injectQuery(() => ({12    queryKey: ['todos'],13    queryFn: () => lastValueFrom(TestBed.inject(HttpClient).get('/api/todos')),14  })),15)16 17const fixturePromise = TestBed.inject(ApplicationRef).whenStable()18httpCtrl.expectOne('/api/todos').flush([{ id: 1 }])19await fixturePromise20 21expect(query.data()).toEqual([{ id: 1 }])22httpCtrl.verify()

无限查询与分页

whenStable() 可能在无限查询的各页数据抵达 data() 信号前即已解析。使用模拟计时器时,应改为将计时器推进至超过查询函数的延迟,调用 fetchNextPage(),再次推进计时器,然后再对 data().pages 进行断言。在 afterEach 中恢复真实计时器,以便即使断言失败也能确保其被恢复:

TypeScript
1beforeEach(() => {2  vi.useFakeTimers()3})4 5afterEach(() => {6  vi.useRealTimers()7})8 9it('should load the next page', async () => {10  const infinite = TestBed.runInInjectionContext(() =>11    injectInfiniteQuery(() => ({12      queryKey: ['pages'],13      queryFn: ({ pageParam }) =>14        new Promise<string>((resolve) =>15          setTimeout(() => resolve(`page ${pageParam}`), 10),16        ),17      initialPageParam: 1,18      getNextPageParam: (_lastPage, allPages) => allPages.length + 1,19    })),20  )21 22  await vi.advanceTimersByTimeAsync(11)23  expect(infinite.data()?.pages).toEqual(['page 1'])24 25  void infinite.fetchNextPage()26  await vi.advanceTimersByTimeAsync(11)27  expect(infinite.data()?.pages).toEqual(['page 1', 'page 2'])28})

修改与乐观更新

whenStable() 可能在通过 mutate() 启动的修改完成前即已解析。如上文示例所示启用模拟计时器后,应在断言结果前将计时器推进至超过修改函数的延迟:

TypeScript
1it('should uppercase the input', async () => {2  const mutation = TestBed.runInInjectionContext(() =>3    injectMutation(() => ({4      mutationFn: (input: string) =>5        new Promise<string>((resolve) =>6          setTimeout(() => resolve(input.toUpperCase()), 10),7        ),8    })),9  )10 11  mutation.mutate('test')12  await vi.advanceTimersByTimeAsync(11)13 14  expect(mutation.isSuccess()).toBe(true)15  expect(mutation.data()).toBe('TEST')16})

快速检查清单

  • 每个测试使用全新的 QueryClient(并在之后清空)
  • 禁用或控制重试以避免超时
  • 使用模拟计时器时,在 whenStable() 前推进计时器 + 微任务
  • 使用 provideHttpClientTesting() 或你偏好的模拟工具断言网络调用
  • 每次执行 refetch、fetchNextPage() 或修改后,应推进计时器超过查询或修改函数的延迟,而非等待 whenStable()
  • 服务测试优先选用 TestBed.runInInjectionContext,组件测试则优先选用 TestBed.createComponent 或 Angular Testing Library 的 render 配合 fixture.detectChanges()

本页为 TanStack Query 文档(Angular 版)的非官方中文翻译,原文由上游团队维护,遵循 MIT 许可。 译文如有偏差,以英文原文为准。

译自上游 59d11de