In Angular tests, how do fixture.detectChanges, whenStable and autoDetectChanges differ, and which one fits a zoneless test?
answer
- force now versus wait for Angular
- pending tasks decide stability
- auto-detect is always on when zoneless
- a second pass hunts changed bindings
basics
~20 sdetectChanges forces a synchronous change detection pass plus a dev-mode check; whenStable returns a promise that settles once Angular has no pending work; autoDetectChanges makes the fixture refresh itself like an app. Zoneless tests auto-detect by default, so you await whenStable.
solid answer
~30 s`fixture.detectChanges()` runs change detection **now** and, unless called as `detectChanges(false)`, follows it with a `checkNoChanges` pass that throws NG0100 if a binding changed. `await fixture.whenStable()` waits until Angular has no pending tasks — a scheduled render, an HttpClient request, a navigation — and resolves immediately if there are none. `autoDetectChanges()` attaches the fixture so it refreshes on the same notifications an app uses. TestBed runs zoneless when zone.js is not loaded, and there fixtures auto-detect by default: `autoDetectChanges(false)` throws. So zoneless tests change state through signals, inputs or events and `await fixture.whenStable()`; `detectChanges()` still works but can hide a missing notification.
code
ts · 7 lines// Zone-based suite (existing code)
fixture.componentInstance.increment();
fixture.detectChanges();
// Zoneless suite (new code)
fixture.componentInstance.increment();
await fixture.whenStable();go deeper
Know that detectChanges refreshes right away, whenStable waits for Angular, and that zoneless tests usually await whenStable after each change.
Explain what counts as pending work, why auto-detect is forced on when zoneless, and what the checkNoChanges pass inside detectChanges catches.
Choose the wait that exercises the real notification path, recognise when detectChanges is masking a missing notification, and read NG0100 as a real defect.
Decide the suite-wide convention during a zoneless migration: which specs are rewritten to whenStable, which keep detectChanges, and how the choice is enforced in review.
## Three ways to get a view in sync A component test changes state and then asserts on the DOM. Between those steps, Angular's change detection has to run. `ComponentFixture` offers three ways to make that happen, and which one fits depends on whether the test runs with zone.js. | API | What it does | Returns | |---|---|---| | `detectChanges(checkNoChanges = true)` | Runs change detection synchronously, then a dev-mode `checkNoChanges` pass | `void` | | `whenStable()` | Waits until Angular reports no pending work | `Promise` (`false` if already stable, `true` after waiting) | | `autoDetectChanges()` | Attaches the fixture to automatic change detection and runs one pass immediately | `void` | ## detectChanges: force it now `detectChanges()` is the imperative option. What it does internally depends on the mode: - **Zone-based tests:** it runs inside `NgZone`, flushes root effects, checks the component's view, then runs `checkNoChanges`. - **Zoneless tests:** it calls `ApplicationRef.tick()` with every test view included, which refreshes the views that were **notified** as needing it, followed by the same dev-mode check. The `checkNoChanges` pass re-evaluates bindings and throws **NG0100** (`ExpressionChangedAfterItHasBeenCheckedError`) if one produced a different value the second time. `detectChanges(false)` skips that pass — rarely a good idea, because the check is how tests catch unstable bindings. ## whenStable: wait for Angular `whenStable()` looks at Angular's **pending tasks**. If there are none, it resolves right away with `false`. Otherwise it resolves with `true` once `ApplicationRef` reports stability. In a zoneless app, pending tasks include: - a change detection pass the scheduler has queued after a notification (a signal write read by a template, `setInput`, a template event listener, `markForCheck`); - work Angular tracks through `PendingTasks`, such as an in-flight `HttpClient` request or a router navigation. Because the scheduled render counts as a pending task, `await fixture.whenStable()` after a state change is exactly "let Angular do what it would do in the app, then assert". If Angular catches an application error while a `whenStable()` promise is pending, that promise rejects with the error (TestBed's default `rethrowApplicationErrors` behaviour). ## autoDetectChanges: behave like the app `autoDetectChanges()` attaches the fixture's view to automatic change detection, so it refreshes whenever Angular would refresh an application, and runs `detectChanges()` once immediately. You can also enable it for every fixture by providing the `ComponentFixtureAutoDetect` token as `true`. - **Zone-based tests:** auto-detect is **off** by default; you opt in. - **Zoneless tests:** auto-detect is **on** by default. `autoDetectChanges(false)` throws "Cannot set autoDetect to false with zoneless change detection."; the boolean overload is deprecated anyway. ## When is a test zoneless? Angular's zoneless guide states that TestBed uses zone-based change detection when `zone.js` is loaded through the test polyfills, and runs zoneless when it is not. `provideZonelessChangeDetection()` in the testing module forces zoneless even with zone.js present. New CLI projects have been zoneless since v21, so their tests are too. ## Which to use 1. **Zoneless (new code):** change state the way the app would, then `await fixture.whenStable()`. The guide asks you to avoid `detectChanges()` where possible: it forces a refresh Angular might not have scheduled, so a component that forgets to notify Angular can still pass. 2. **Zone-based (existing suites):** `fixture.detectChanges()` after each change is the established pattern, and the guide says converting large suites is usually not worth the effort. 3. **Either:** keep the `checkNoChanges` pass on; it is part of what the test verifies. ```ts it('increments on click', async () => { const fixture = TestBed.createComponent(LimitedCounter); await fixture.whenStable(); fixture.nativeElement.querySelector('button').click(); await fixture.whenStable(); expect(fixture.nativeElement.querySelector('.count').textContent).toBe('1'); }); ``` ## Turning auto-detect on everywhere In a zone-based suite that wants application-like behaviour for every spec, provide the `ComponentFixtureAutoDetect` token instead of calling `autoDetectChanges()` in each test: ```ts TestBed.configureTestingModule({ providers: [{provide: ComponentFixtureAutoDetect, useValue: true}], }); ``` Every fixture created afterwards starts attached. Tests still need a wait after a change, because automatic change detection happens after the change, not inside the assignment. ## Traps - Calling `detectChanges()` once and assuming asynchronous work (an HTTP response, a resolved promise) has landed; that needs a wait. - Expecting `whenStable()` to fast-forward timers; it waits for them. - Treating an NG0100 from `detectChanges()` as test noise rather than a real unstable binding.
- Why does the zoneless guide discourage fixture.detectChanges()?It forces a refresh even when nothing notified Angular. In the app, a component that changes state without a signal write, `markForCheck` or an event would never refresh; a test that calls `detectChanges()` can hide that bug. Awaiting `whenStable()` only renders what Angular itself scheduled, so the test exercises the real notification path.
- Why can await fixture.whenStable() resolve immediately with false while the DOM is still stale?`whenStable()` resolves at once, with `false`, when Angular has no pending tasks. If the test changed state without notifying Angular — for example by assigning a plain field on an `OnPush` component — no render was scheduled, so there is nothing to wait for. Change state through a signal, `setInput` or an event, and the scheduled render becomes the pending task the promise waits on.
detectChanges is walking to the notice-board and reading it right now; whenStable is waiting until the person pinning sheets says they are done; autoDetectChanges is subscribing to the board so you are told whenever it changes.
saying these in an interview costs you the question
- whenStable fast-forwards pending timers so the test does not wait.
- In zoneless tests you must call detectChanges after every change.
- autoDetectChanges(false) is the way to take manual control in a zoneless test.
- detectChanges(false) is a harmless speed-up worth using everywhere.
- Zoneless mode is enabled only by adding provideZonelessChangeDetection to each test.