
TanStack Query for Angular 测试实战PendingTasks 集成、TestBed 策略与源码级原理【免费下载链接】query Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query在 Angular 应用中测试基于injectQuery/injectMutation的服务或组件核心难点是如何可靠地等待异步数据就绪。本文围绕 TanStack Query Angular 包的官方测试指南展开先讲清inject*函数如何通过 Angular 19 引入的PendingTasks机制让ApplicationRef.whenStable()/fixture.whenStable()天然感知进行中的查询与变更再给出 TestBed 配置、首个查询测试、组件测试、重试控制、HttpClientTestingModule网络桩、无限查询分页、变更与乐观更新等完整可复制的测试代码并结合tanstack/angular-query-experimental包的源码pending-tasks-compat.ts、create-base-query.ts、providers.ts与官方测试套件pending-tasks.test.ts说明每段模式背后的实现依据。读完本文你可以独立搭建一套在 Zone.js 与 Zoneless 两种变更检测模式下都能稳定运行的 Angular Query 单元测试。一、原理先行PendingTasks 如何让 whenStable() 等待查询完成文档开篇指出大多数使用 TanStack Query 的 Angular 测试都会涉及调用injectQuery/injectMutation的服务或组件。TanStack Query 的inject*函数与 Angular 的PendingTasks机制集成确保框架知道有哪些查询和变更正在进行中。这意味着测试以及 SSR可以等待查询和变更全部落地单元测试中可以用ApplicationRef.whenStable()或fixture.whenStable()来等待查询完成该做法在 Zone.js 和 Zoneless 两种配置下都有效版本前提该集成要求 Angular 19 或更高版本。更早的 Angular 版本不支持PendingTasks。源码证据PENDING_TASKS 注入令牌与降级方案这个集成在 Angular 包中由 pending-tasks-compat.ts 实现。它定义了一个PENDING_TASKS注入令牌工厂函数通过Reflect.get(ng, PendingTasks)动态探测 Angular 是否提供了PendingTasksAPIexport const PENDING_TASKS new InjectionTokenPendingTasksCompat( PENDING_TASKS, { factory: (): PendingTasksCompat { // 通过 Reflect 访问令打包器在令牌缺失时Angular 19保持安静 const token Reflect.get(ng, PendingTasks) as unknown as ... const svc token ? (inject(token, { optional: true }) ...) : null // 没有 PendingTasks 时回退到一个稳定的 no-op shim return { add: svc ? () svc.add() : () noop } }, }, )从源码结构看这正是文档中Angular 19 才支持警告的落地方式低版本 Angular 下不会报错只是 PendingTasks 记录被降级为 no-op此时whenStable()不再阻塞查询测试需退化为手动等待打包器也因此不会把缺失的 API 当作编译错误。查询侧fetching 时注册任务idle 时释放create-base-query.ts 是injectQuery与injectInfiniteQuery的共享基座见 inject-query.ts 中createBaseQuery(injectQueryFn, QueryObserver)的调用。其核心 effect 订阅 observer 后根据fetchStatus维护一个 pending task 引用notifyManager.batchCalls((state) { ngZone.run(() { if (state.fetchStatus fetching !pendingTaskRef) { pendingTaskRef pendingTasks.add() // 查询开始登记一个未完成任务 } if (state.fetchStatus idle pendingTaskRef) { pendingTaskRef() // 查询结束释放任务 pendingTaskRef null } ... resultFromSubscriberSignal.set(state) }) }),onCleanup中同样会释放pendingTaskRef并取消订阅所以组件被销毁后不会遗留僵尸任务导致whenStable()永远挂起——官方测试套件中专门有用例验证了这一点见第六节。变更侧同理inject-mutation.ts 中也通过injector.get(PENDING_TASKS)登记进行中的 mutation。理解了这条链路文档中所有awaitwhenStable()的写法就都有了实现依据查询处于fetching状态期间会注册 pending task状态回到idle时释放whenStable()因此能精确地等到数据落地。二、TestBed 配置为每个 spec 准备干净的 QueryClient文档建议为每个 spec 创建一个全新的QueryClient并通过provideTanStackQuery或provideQueryClient提供。这样能保持缓存隔离并允许按测试修改默认选项const queryClient new QueryClient({ defaultOptions: { queries: { retry: false, // ✅ 让失败用例更快 }, }, }) TestBed.configureTestingModule({ providers: [provideTanStackQuery(queryClient)], })文档还特别警告如果单元测试直接复用了应用真实的 TanStack Query 配置务必确认withDevtools没有意外进入测试 providers——它会拖慢测试最好让测试与生产配置分离。从源码看这个警告是合理的providers.ts 中provideTanStackQuery(queryClient, ...features)会把features如withDevtools()返回的DevtoolsFeature展开为额外的 provider 数组而provideQueryClient内部还会在工厂中执行client.mount()并通过DestroyRef.onDestroy调用client.unmount()保证注入器销毁时清理——所以为每个测试注入一个独立实例生命周期管理是自动闭合的。此外若测试共享了辅助函数文档要求在afterEach中调用queryClient.clear()或重建实例确保一个测试的数据不会渗入下一个测试。官方测试套件 pending-tasks.test.ts 正是这样做的beforeEach(() { vi.useFakeTimers() queryClient new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }) TestBed.configureTestingModule({ providers: [provideZonelessChangeDetection(), provideTanStackQuery(queryClient)], }) }) afterEach(() { onlineManager.setOnline(true) queryClient.clear() vi.useRealTimers() })注意两个细节defaultOptions同时关闭了queries与mutations的重试afterEach里除了clear()还恢复了onlineManager的在线状态并换回真实计时器——如果某个用例通过onlineManager.setOnline(false)模拟离线不做恢复会污染后续用例。三、第一个查询测试runInInjectionContext tick whenStable文档给出的标准模式是查询测试通常在TestBed.runInInjectionContext中执行然后等待稳定const appRef TestBed.inject(ApplicationRef) const query TestBed.runInInjectionContext(() injectQuery(() ({ queryKey: [greeting], queryFn: () Hello, })), ) TestBed.tick() // 触发 effect // 查询空闲时应用即稳定 await appRef.whenStable() expect(query.status()).toBe(success) expect(query.data()).toBe(Hello)三段式各有其源码层面的原因TestBed.runInInjectionContextinjectQuery首行会执行assertInInjectionContext(injectQuery)见 inject-query.ts必须在注入上下文中调用TestBed.tick()从源码结构看create-base-query.ts中订阅 observer 的逻辑放在effect里effect 的初次执行需要等到下一轮变更检测tick()正是用来驱动这一轮否则查询根本不会启动await appRef.whenStable()如第一节所述fetching期间注册的 pending task 保证此 Promise 在查询落地后才 resolve。使用 fake timersVitest时的注意事项PendingTasks 会让whenStable()在查询 settle 之后才 resolve若使用 Vitest 的 fake timers需要先拨表并冲掉一个微任务再等待稳定await vi.advanceTimersByTimeAsync(0) await Promise.resolve() await appRef.whenStable()官方测试套件中的完整节奏是先取 stablePromise → 冲微任务 → 拨表 → await stablePromiseconst stablePromise app.whenStable() // 冲掉微任务让 TanStack Query 的调度通知得以处理 await Promise.resolve() await vi.advanceTimersByTimeAsync(10) await stablePromise这个顺序并非随意Promise.resolve()先让 query-core 里经notifyManager排程的回调有机会执行状态翻转为idle并释放 pending task再拨表让定时逻辑推进最后stablePromise自然 resolve。同步 resolve 的queryFn与同步抛错的queryFn都能被该模式正确覆盖分别断言success与error状态。四、组件测试TestBed.createComponent fixture.whenStable对于组件文档建议用TestBed.createComponent引导然后等待fixture.whenStable()const fixture TestBed.createComponent(ExampleComponent) await fixture.whenStable() expect(fixture.componentInstance.query.data()).toEqual({ value: 42 })组件场景下 fixture 同时承担了 effect 触发与稳定性等待两件事因此比服务测试更简洁。若组件带有 signal 输入仓库测试工具 test-utils.ts 提供了setFixtureSignalInputs辅助函数它借助angular/core/primitives/signals中的signalSetFn直接写入input()signal 的值绕过angular/angular#54013的限制并按需调用detectChanges()可作为自定义组件测试工具的实现参考。五、重试控制别让 backoff 拖垮测试文档指出重试会拖慢失败用例因为默认 backoff 会重试三次。应通过defaultOptions或单个 query 设置retry: false或指定次数保持测试快速若某个查询确实要验证重试逻辑应断言最终状态而非中间次数。官方测试套件提供了确实要验证重试时的写法显式给小retryDelay用 fake timers 拨过全部重试窗口最后断言尝试次数与终态const query TestBed.runInInjectionContext(() injectQuery(() ({ queryKey: key, retry: 2, retryDelay: 10, queryFn: async () { attemptCount if (attemptCount 2) throw new Error(Attempt ${attemptCount} failed) return success-data }, })), ) const stablePromise app.whenStable() await vi.advanceTimersByTimeAsync(50) await stablePromise expect(query.status()).toBe(success) expect(attemptCount).toBe(3) // 首次 2 次重试六、HttpClient 与网络桩HttpTestingController 与 PendingTasks 配合文档说明Angular 的HttpClientTestingModule与 PendingTasks 配合良好。将其与 Query provider 一起注册再通过HttpTestingController冲刷响应即可TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [provideTanStackQuery(queryClient)], }) const httpCtrl TestBed.inject(HttpTestingController) const query TestBed.runInInjectionContext(() injectQuery(() ({ queryKey: [todos], queryFn: () lastValueFrom(TestBed.inject(HttpClient).get(/api/todos)), })), ) const fixturePromise TestBed.inject(ApplicationRef).whenStable() httpCtrl.expectOne(/api/todos).flush([{ id: 1 }]) await fixturePromise expect(query.data()).toEqual([{ id: 1 }]) httpCtrl.verify()注意这里的时序技巧先取whenStable()Promise再flush响应——若先 flush 再取查询可能已经完成whenStable()会立即 resolve 而错过等待数据的语义。httpCtrl.verify()收尾则保证没有未处理的挂起请求。仓库测试套件 pending-tasks.test.ts 的 HttpClient Integration 分组还覆盖了两类更复杂的场景并发请求两个injectQuery分别请求/api/1、/api/2用setTimeout fake timers 在 10ms 后依次expectOne().flush()最后断言两者均success且httpTestingController.verify()通过请求失败/取消对/api/cancel调用req.error(new ProgressEvent(error), { status: 0, statusText: Unknown Error })模拟网络错误断言查询进入error状态。此外Edge Cases 分组验证了queryClient.cancelQueries({ queryKey })在飞行中取消后查询会恢复到pending/idle预取状态且whenStable()依然正常 resolve——测试中取消飞行中的查询不会破坏 PendingTasks 的记账。七、无限查询与分页文档说明无限查询使用同一套模式——调用fetchNextPage()若在使用 fake 时间则拨表然后等待稳定并断言data().pagesconst infinite TestBed.runInInjectionContext(() injectInfiniteQuery(() ({ queryKey: [pages], queryFn: ({ pageParam 1 }) fetchPage(pageParam), getNextPageParam: (last, all) all.length 1, })), ) await appRef.whenStable() expect(infinite.data().pages).toHaveLength(1) await infinite.fetchNextPage() await vi.advanceTimersByTimeAsync(0) await appRef.whenStable() expect(infinite.data().pages).toHaveLength(2)关键点fetchNextPage()每次都会把 fetchStatus 重新带入fetching因此每一页加载后都需要重新等待一次whenStable()若queryFn是异步的还要先用vi.advanceTimersByTimeAsync(0)冲掉定时器再等稳定。八、变更Mutations与乐观更新文档给出的变更测试模式const mutation TestBed.runInInjectionContext(() injectMutation(() ({ mutationFn: async (input: string) input.toUpperCase(), })), ) mutation.mutate(test) // 触发 effect TestBed.tick() await appRef.whenStable() expect(mutation.isSuccess()).toBe(true) expect(mutation.data()).toBe(TEST)mutate之后同样需要TestBed.tick()触发 effect 使订阅建立再等待稳定。若还要验证乐观更新与回滚仓库测试套件提供了一个完整可参考的用例结构onMutate中先保存旧值并写入新值onError中回滚断言分三层——mutate后立刻验证数据已被乐观改写此时尚未稳定、whenStable()后验证isSuccess()与最终缓存值queryClient.setQueryData(testQueryKey, initial-data) const mutation TestBed.runInInjectionContext(() injectMutation(() ({ mutationFn: (newData: string) sleep(50).then(() newData), onMutate: async (newData) { const previousData queryClient.getQueryData(testQueryKey) queryClient.setQueryData(testQueryKey, newData) return { previousData } }, onError: (_err, _newData, context) { if (context?.previousData) { queryClient.setQueryData(testQueryKey, context.previousData) } }, })), ) mutation.mutate(optimistic-data) await Promise.resolve() expect(queryClient.getQueryData(testQueryKey)).toBe(optimistic-data) // 乐观值即时可见 // ...await appRef.whenStable() 后断言 isSuccess() 与终态九、从官方测试套件提取的进阶模式除文档列出的场景外pending-tasks.test.ts 还验证了若干生产测试中容易踩坑的行为值得作为补充组件销毁不会卡死 whenStable用Component定义一个持有查询与变更的TestComponentTestBed.createComponent后立即fixture.destroy()随后断言app.whenStable()在拨表后 resolve。这印证了 create-base-query.ts 中onCleanup释放 pending task、inject-mutation.ts的清理逻辑组件消失后它未完成的异步操作不会继续阻塞框架稳定性。离线暂停期间 pending task 持续阻塞当查询重试进入延迟期时调用onlineManager.setOnline(false)fetch 会被暂停fetchStatus paused。测试断言此时whenStable()不会resolve恢复在线后重试继续并最终成功stablePromise才 resolve。即暂停中的重试同样被视为未完成的框架工作与查询执行中的行为一致。并发场景套件中的 Concurrent Operations 分组验证了 3 个不同 key 的查询并发其中一个是同步queryFn、3 个变更并发、以及查询与变更混合并发全部通过一次advanceTimersByTimeAsyncstablePromise完成断言。快速连续refetch()三次也不产生任务泄漏最终status为success。这些用例与文档清单共同说明每次状态变更后 await 一次whenStable() 在并发、取消、离线、销毁等边界下依然成立可以放心作为测试的默认同步手段。十、快速检查清单汇总文档的 Quick checklist并补充仓库测试实践验证过的两条每个测试使用全新的QueryClient测试后clear()或重建实例关闭或控制重试避免 backoff 拖成超时使用 fake timers 时whenStable()前先拨表 冲微任务使用HttpClientTestingModule或你偏好的 mock 来断言网络调用并用verify()收尾每次refetch、fetchNextPage或 mutation 之后都要await whenStable()服务测试优先TestBed.runInInjectionContext组件测试优先fixture.whenStable()补充mutate/injectQuery建立订阅依赖 effect 执行必要时先TestBed.tick()再等待稳定补充模拟离线或全局状态的用例在afterEach中恢复如onlineManager.setOnline(true)进一步深入原理与降级实现pending-tasks-compat.ts、create-base-query.tsprovider 生命周期mount/unmountproviders.ts完整的 PendingTasks 集成测试本文多数进阶用例的出处pending-tasks.test.ts组件测试工具函数signal 输入写入test-utils.ts应用端配置参考installation.md【免费下载链接】query Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考