|
| 1 | +--- |
| 2 | +id: testing |
| 3 | +title: Testing |
| 4 | +--- |
| 5 | + |
| 6 | +Most Angular tests using TanStack Query will involve services or components that call `injectQuery`/`injectMutation`. |
| 7 | + |
| 8 | +TanStack Query's `inject*` functions integrate with [`PendingTasks`](https://angular.dev/api/core/PendingTasks) which ensures the framework is aware of in-progress queries and mutations. |
| 9 | + |
| 10 | +This means tests and SSR can wait until mutations and queries resolve. In unit tests you can use `ApplicationRef.whenStable()` or `fixture.whenStable()` to await query completion. This works for both Zone.js and Zoneless setups. |
| 11 | + |
| 12 | +> This integration requires Angular 19 or later. Earlier versions of Angular do not support `PendingTasks`. |
| 13 | +
|
| 14 | +## TestBed setup |
| 15 | + |
| 16 | +Create a fresh `QueryClient` for every spec and provide it with `provideTanStackQuery` or `provideQueryClient`. This keeps caches isolated and lets you change default options per test: |
| 17 | + |
| 18 | +```ts |
| 19 | +const queryClient = new QueryClient({ |
| 20 | + defaultOptions: { |
| 21 | + queries: { |
| 22 | + retry: false, // ✅ faster failure tests |
| 23 | + }, |
| 24 | + }, |
| 25 | +}) |
| 26 | + |
| 27 | +TestBed.configureTestingModule({ |
| 28 | + providers: [provideTanStackQuery(queryClient)], |
| 29 | +}) |
| 30 | +``` |
| 31 | + |
| 32 | +> If your applications actual TanStack Query config is used in unit tests, make sure `withDevtools` is not accidentally included in test providers. This can cause slow tests. It is best to keep test and production configs separate. |
| 33 | +
|
| 34 | +If you share helpers, remember to call `queryClient.clear()` (or build a new instance) in `afterEach` so data from one test never bleeds into another. |
| 35 | + |
| 36 | +## First query test |
| 37 | + |
| 38 | +Query tests typically run inside `TestBed.runInInjectionContext`, then wait for stability: |
| 39 | + |
| 40 | +```ts |
| 41 | +const appRef = TestBed.inject(ApplicationRef) |
| 42 | +const query = TestBed.runInInjectionContext(() => |
| 43 | + injectQuery(() => ({ |
| 44 | + queryKey: ['greeting'], |
| 45 | + queryFn: () => 'Hello', |
| 46 | + })), |
| 47 | +) |
| 48 | + |
| 49 | +TestBed.tick() // Trigger effect |
| 50 | + |
| 51 | +// Application is stable when queries are idle |
| 52 | +await appRef.whenStable() |
| 53 | + |
| 54 | +expect(query.status()).toBe('success') |
| 55 | +expect(query.data()).toBe('Hello') |
| 56 | +``` |
| 57 | + |
| 58 | +PendingTasks will have `whenStable()` resolve after the query settles. When using fake timers (Vitest), advance the clock and a microtask before awaiting stability: |
| 59 | + |
| 60 | +```ts |
| 61 | +await vi.advanceTimersByTimeAsync(0) |
| 62 | +await Promise.resolve() |
| 63 | +await appRef.whenStable() |
| 64 | +``` |
| 65 | + |
| 66 | +## Testing components |
| 67 | + |
| 68 | +For components, bootstrap them through `TestBed.createComponent`, then await `fixture.whenStable()`: |
| 69 | + |
| 70 | +```ts |
| 71 | +const fixture = TestBed.createComponent(ExampleComponent) |
| 72 | + |
| 73 | +await fixture.whenStable() |
| 74 | +expect(fixture.componentInstance.query.data()).toEqual({ value: 42 }) |
| 75 | +``` |
| 76 | + |
| 77 | +## Handling retries |
| 78 | + |
| 79 | +Retries slow failing tests because the default backoff runs three times. Set `retry: false` (or a specific number) through `defaultOptions` or per query to keep tests fast. If a query intentionally retries, assert on the final state rather than intermediate counts. |
| 80 | + |
| 81 | +## HttpClient & network stubs |
| 82 | + |
| 83 | +Angular's `HttpClientTestingModule` plays nicely with PendingTasks. Register it alongside the Query provider and flush responses through `HttpTestingController`: |
| 84 | + |
| 85 | +```ts |
| 86 | +TestBed.configureTestingModule({ |
| 87 | + imports: [HttpClientTestingModule], |
| 88 | + providers: [provideTanStackQuery(queryClient)], |
| 89 | +}) |
| 90 | + |
| 91 | +const httpCtrl = TestBed.inject(HttpTestingController) |
| 92 | +const query = TestBed.runInInjectionContext(() => |
| 93 | + injectQuery(() => ({ |
| 94 | + queryKey: ['todos'], |
| 95 | + queryFn: () => lastValueFrom(TestBed.inject(HttpClient).get('/api/todos')), |
| 96 | + })), |
| 97 | +) |
| 98 | + |
| 99 | +const fixturePromise = TestBed.inject(ApplicationRef).whenStable() |
| 100 | +httpCtrl.expectOne('/api/todos').flush([{ id: 1 }]) |
| 101 | +await fixturePromise |
| 102 | + |
| 103 | +expect(query.data()).toEqual([{ id: 1 }]) |
| 104 | +httpCtrl.verify() |
| 105 | +``` |
| 106 | + |
| 107 | +## Infinite queries & pagination |
| 108 | + |
| 109 | +Use the same pattern for infinite queries: call `fetchNextPage()`, advance timers if you are faking time, then await stability and assert on `data().pages`. |
| 110 | + |
| 111 | +```ts |
| 112 | +const infinite = TestBed.runInInjectionContext(() => |
| 113 | + injectInfiniteQuery(() => ({ |
| 114 | + queryKey: ['pages'], |
| 115 | + queryFn: ({ pageParam = 1 }) => fetchPage(pageParam), |
| 116 | + getNextPageParam: (last, all) => all.length + 1, |
| 117 | + })), |
| 118 | +) |
| 119 | + |
| 120 | +await appRef.whenStable() |
| 121 | +expect(infinite.data().pages).toHaveLength(1) |
| 122 | + |
| 123 | +await infinite.fetchNextPage() |
| 124 | +await vi.advanceTimersByTimeAsync(0) |
| 125 | +await appRef.whenStable() |
| 126 | + |
| 127 | +expect(infinite.data().pages).toHaveLength(2) |
| 128 | +``` |
| 129 | + |
| 130 | +## Mutations and optimistic updates |
| 131 | + |
| 132 | +```ts |
| 133 | +const mutation = TestBed.runInInjectionContext(() => |
| 134 | + injectMutation(() => ({ |
| 135 | + mutationFn: async (input: string) => input.toUpperCase(), |
| 136 | + })), |
| 137 | +) |
| 138 | + |
| 139 | +mutation.mutate('test') |
| 140 | + |
| 141 | +// Trigger effect |
| 142 | +TestBed.tick() |
| 143 | + |
| 144 | +await appRef.whenStable() |
| 145 | + |
| 146 | +expect(mutation.isSuccess()).toBe(true) |
| 147 | +expect(mutation.data()).toBe('TEST') |
| 148 | +``` |
| 149 | + |
| 150 | +## Quick checklist |
| 151 | + |
| 152 | +- Fresh `QueryClient` per test (and clear it afterwards) |
| 153 | +- Disable or control retries to avoid timeouts |
| 154 | +- Advance timers + microtasks before `whenStable()` when using fake timers |
| 155 | +- Use `HttpClientTestingModule` or your preferred mock to assert network calls |
| 156 | +- Await `whenStable()` after every `refetch`, `fetchNextPage`, or mutation |
| 157 | +- Prefer `TestBed.runInInjectionContext` for service tests and `fixture.whenStable()` for component tests |
0 commit comments