Skip to the solution
EasyFrontend9 min read

What is the fixture.detectChanges() method in Angular testing, and when should you call it?

125 views

The problem

Question presented to candidate: "In a TestBed spec you create a component and the DOM is empty. What does fixture.detectChanges() actually do, when do you need to call it, and is it still the right tool in a zoneless Angular 22 project?"

What a strong answer should cover:

  • TestBed.createComponent() builds the component and its host element but does not render bindings; change detection has to run first.
  • fixture.detectChanges() runs one synchronous change detection pass for the fixture and then, in dev mode, a checkNoChanges() pass that throws NG0100 if a value changed after it was checked.
  • Under OnPush (the v22 default), detectChanges() only refreshes views that are dirty: signals, template events, setInput() and markForCheck() mark them; a plain field assignment does not.
  • In a zoneless TestBed (the CLI default), the fixture already auto-detects; the generated spec uses await fixture.whenStable() to let Angular render the way the app would.
  • When to prefer which: detectChanges() for a precise, synchronous step (and with fake timers); whenStable() for realistic, async flows.
  • Seeing NG0100 in a zoneless test usually means state was changed without notifying Angular.

Clarifying questions expected:

  • "Is the project zoneless, or does it still load Zone.js?"
  • "Are the components OnPush (the v22 default) or Eager?"

Code / implementation expected: Yes β€” a spec showing the first render, a signal update, the OnPush plain-field trap and an NG0100.

  • testing
  • change-detection

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 9 sections

Read the code

AngulardetectChanges() vs whenStable(), OnPush and NG0100 (Vitest, ran with ng test on Angular 22.2.3)
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';

@Component({
  selector: 'app-badge',
  template: `<p class="name">{{ name }}</p><p class="count">{{ count() }}</p>`,
})
class Badge {
  cdr = inject(ChangeDetectorRef);
  name = 'Ada'; // a plain field
  count = signal(0); // a signal
}

@Component({
  selector: 'app-late-writer',
  changeDetection: ChangeDetectionStrategy.Eager,
  template: `<p>{{ label }}</p>`,
})
class LateWriter {
  label = 'first';
  ngAfterViewInit() {
    this.label = 'second'; // changes a value the view already rendered
  }
}

const text = (f: { nativeElement: HTMLElement }, css: string) => f.nativeElement.querySelector(css)?.textContent;

describe('fixture.detectChanges()', () => {
  it('nothing is rendered until the first change detection', () => {
    const fixture = TestBed.createComponent(Badge);
    expect(fixture.nativeElement.textContent).toBe('');
    fixture.detectChanges();
    expect(text(fixture, '.name')).toBe('Ada');
  });

  it('signals: detectChanges() renders synchronously, whenStable() asynchronously', async () => {
    const fixture = TestBed.createComponent(Badge);
    fixture.detectChanges();

    fixture.componentInstance.count.set(5);
    expect(text(fixture, '.count')).toBe('0'); // not yet
    fixture.detectChanges();
    expect(text(fixture, '.count')).toBe('5'); // now, synchronously

    fixture.componentInstance.count.set(6);
    await fixture.whenStable(); // let the zoneless scheduler do it
    expect(text(fixture, '.count')).toBe('6');
  });

  it('OnPush (the v22 default): a plain field change needs markForCheck()', () => {
    const fixture = TestBed.createComponent(Badge);
    fixture.detectChanges();

    fixture.componentInstance.name = 'Grace';
    fixture.detectChanges();
    expect(text(fixture, '.name')).toBe('Ada'); // the view was not dirty, so it was skipped

    fixture.componentInstance.cdr.markForCheck(); // the component's own ChangeDetectorRef
    fixture.detectChanges();
    expect(text(fixture, '.name')).toBe('Grace');
  });

  it('detectChanges() also runs checkNoChanges() in dev mode', () => {
    const fixture = TestBed.createComponent(LateWriter);
    expect(() => fixture.detectChanges()).toThrow(/NG0100: ExpressionChangedAfterItHasBeenCheckedError/);
  });
});

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 12 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextEasy

What are the types of data binding in Angular?

Continue