测试
大多数使用 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 提供它。这可使缓存相互隔离,并允许你为每个测试单独更改默认选项:
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 内运行,然后等待稳定性:
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)时,在等待稳定性前需推进计时器和微任务:
1await vi.advanceTimersByTimeAsync(0)2await Promise.resolve()3await appRef.whenStable()组件测试
对于组件,请通过 TestBed.createComponent 创建它们。fixture.whenStable() 可能在查询数据到达前即已解析;因此在启用模拟计时器时,应先将计时器推进至超过查询函数的延迟,再运行 fixture.detectChanges() 并断言组件的渲染结果:
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 运行变更检测:
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 创建它,以相同方式推进计时器,并对其信号进行断言:
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 刷新响应:
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 中恢复真实计时器,以便即使断言失败也能确保其被恢复:
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() 启动的修改完成前即已解析。如上文示例所示启用模拟计时器后,应在断言结果前将计时器推进至超过修改函数的延迟:
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()