skip to content

Component Fixtures

A ComponentFixture drives a created component: detectChanges, whenStable or autoDetectChanges to settle it, setInput for inputs, DebugElement and By.css to query. Interviewers probe zoneless fixtures.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Angular, what does TestBed.createComponent return, and how do you use that fixture to render and query the component?

level: juniorimportance: must knowfreq 70%

answer

  1. a handle around the created component
  2. class instance versus host element
  3. nothing renders synchronously
  4. By.css through the debug tree

basics

~10 s

TestBed.createComponent returns a ComponentFixture: a handle exposing the component instance, its host element and a DebugElement tree. You let it render with await fixture.whenStable() or fixture.detectChanges(), then query nativeElement or debugElement.query(By.css(...)).

solid answer

~30 s

`TestBed.createComponent(LimitedCounter)` returns a `ComponentFixture<LimitedCounter>`. It exposes `componentInstance` (the class), `componentRef` (for `setInput`), `nativeElement` (the host DOM element), `debugElement` (Angular's queryable wrapper) and methods that drive change detection. Creating the component does **not** render its template synchronously, so you first `await fixture.whenStable()` — the default in zoneless tests — or call `fixture.detectChanges()`. Then query the DOM: `fixture.nativeElement.querySelector('.count')`, or `fixture.debugElement.query(By.css('.count'))`, which returns a `DebugElement` or `null`, and `By.directive(Child)` to find child components.

code

ts · 14 lines
ts
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {LimitedCounter} from './limited-counter';

it('renders the count and the button', async () => {
  const fixture = TestBed.createComponent(LimitedCounter);
  await fixture.whenStable();

  const count = fixture.debugElement.query(By.css('.count'));
  const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');

  expect(count.nativeElement.textContent).toBe('0');
  expect(button.disabled).toBe(false);
});

go deeper

for a junior

Know the fixture's main members, that you must let it render before asserting, and the two ways to query: nativeElement.querySelector and debugElement.query(By.css).

for a middle

Explain when debugElement is worth it over nativeElement: By.directive, element injectors and triggerEventHandler, plus how query and queryAll behave on no match.

for a senior

Keep assertions on rendered output rather than instance internals, and build small helpers (possibly via getLastFixture) so specs read as user actions.

for a principal

Set conventions for which query layer specs use, fixture APIs, CDK harnesses or user-facing queries, and when each is acceptable in review.

## What a fixture is A **`ComponentFixture<T>`** is what `TestBed.createComponent(T)` hands back after creating a component inside the testing module. It is a test-only wrapper around three things Angular created for you: the component instance, the host element it rendered into, and a `ComponentRef` that controls it. Every component test starts by holding one. The running example is a counter with a limit: ```ts import {Component, input, output, signal} from '@angular/core'; @Component({ selector: 'app-limited-counter', template: ` <span class="count">{{ count() }}</span> <button type="button" (click)="increment()" [disabled]="count() >= max()">+</button> `, }) export class LimitedCounter { readonly max = input(3); readonly count = signal(0); readonly limitReached = output<number>(); increment(): void { this.count.update((c) => c + 1); if (this.count() === this.max()) { this.limitReached.emit(this.count()); } } } ``` ## The fixture's members | Member | What it gives you | Typical use | |---|---|---| | `componentInstance` | The `LimitedCounter` class instance | Read `count()`, call `increment()` | | `componentRef` | The `ComponentRef` | `componentRef.setInput('max', 5)` | | `nativeElement` | The host DOM element | `querySelector`, `textContent`, real `click()` | | `debugElement` | A `DebugElement` for the host | `query(By.css(...))`, `injector`, `triggerEventHandler` | | `changeDetectorRef` | The host view's change detector | Rarely needed directly | | `detectChanges()` | Runs change detection now | Older and zone-based tests | | `whenStable()` | Promise that settles when the app is stable | The default wait in zoneless tests | | `destroy()` | Destroys the component | Testing `ngOnDestroy` / `DestroyRef` cleanup | ## Rendering: nothing happens synchronously `TestBed.createComponent` creates the component but does **not** run change detection synchronously. Immediately after it, the `<span class="count">` is not in the DOM yet. You have to let Angular render: - `await fixture.whenStable()` — the pattern the current guide and CLI template use for zoneless tests (the default for new projects since v21). - `fixture.detectChanges()` — runs change detection synchronously; common in older, zone-based suites. ```ts it('starts at zero', async () => { const fixture = TestBed.createComponent(LimitedCounter); await fixture.whenStable(); expect(fixture.nativeElement.querySelector('.count').textContent).toBe('0'); }); ``` ## Querying: nativeElement or debugElement Two routes reach the rendered DOM: 1. **`nativeElement`** is the host element itself. Standard DOM APIs work: `querySelector`, `querySelectorAll`, `textContent`, `click()`. 2. **`debugElement`** wraps the same tree in Angular's `DebugElement`, which adds framework knowledge: - `query(By.css('.count'))` returns the **first** matching `DebugElement`, or `null` when nothing matches. - `queryAll(By.css('button'))` returns an array, empty when nothing matches. - `By.directive(LimitedCounter)` matches elements where a given component or directive is present — handy for finding a child component and reading its `componentInstance`. - `.nativeElement` on any `DebugElement` gets you back to the DOM node. - `.injector` gives the element's injector, which is how you reach a service a component provides for itself. `By` is imported from `@angular/platform-browser`. In browser-based tests, `nativeElement` is enough for most assertions; `debugElement` earns its place when you need `By.directive`, the element injector, or `triggerEventHandler`. ## Getting the fixture back later Angular 22 added `TestBed.getLastFixture()`, which returns the most recently created `ComponentFixture` in the current test and throws "No fixture has been created yet." if there is none. It helps shared helpers that act on "the component under test" without threading the fixture through every call. ## Destroying and cleanup `fixture.destroy()` destroys the component: `ngOnDestroy` runs, `DestroyRef.onDestroy` callbacks fire, and subscriptions tied to them end. That makes cleanup testable — create the fixture, destroy it, then assert that a timer stopped or a service was told to unsubscribe. You rarely need to call it for hygiene: TestBed destroys every active fixture when it resets between tests. A test can create **more than one** fixture, for instance to check two counters with different limits side by side. Each fixture is independent: its own component instance, its own `componentRef`, and its own host element. ## Mistakes interviewers listen for - Asserting on the DOM straight after `createComponent`, before any render. - Treating `query(By.css(...))` as returning an array, or expecting it to throw on no match. - Reaching into private fields of `componentInstance` instead of asserting on what renders. - Forgetting that `nativeElement` is the **host** element, so `querySelector` searches inside it.

  • How would you get the instance of a child component rendered inside the component under test?
    Query for it by type: `fixture.debugElement.query(By.directive(ChildComponent))` returns the child's host `DebugElement`, and its `componentInstance` is the child class. `queryAll(By.directive(...))` returns every instance. That lets you assert what the parent passed in without depending on the child's markup.
  • What does TestBed.getLastFixture() do, and when is it useful?
    Added in Angular 22, it returns the most recently created `ComponentFixture` in the current test, and throws if none exists yet. It is useful in shared helpers, such as a `clickButton(label)` utility, that need the fixture without it being passed through every call.

saying these in an interview costs you the question

  • TestBed.createComponent renders the template synchronously, so you can assert immediately.
  • debugElement.query returns an empty array when nothing matches.
  • nativeElement is the component class instance.
  • By.css is imported from @angular/core/testing.
  • You must read private component fields to verify what the user sees.
open as a page

In Angular tests, how do fixture.detectChanges, whenStable and autoDetectChanges differ, and which one fits a zoneless test?

level: middleimportance: must knowfreq 62%

basics

~20 s

detectChanges 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.

open as a page

In an Angular 22 test, why can setting a field on fixture.componentInstance and calling detectChanges leave the DOM stale, and what should you do instead?

level: middleimportance: should knowfreq 52%

basics

~20 s

Since v22 a component without a changeDetection value is OnPush, and assigning a plain field notifies nothing, so change detection skips the view. Drive state through signals, set inputs with fixture.componentRef.setInput, or bind them with inputBinding.

open as a page

In Angular tests, how does DebugElement.triggerEventHandler differ from dispatching a real DOM event, and when does the difference hide bugs?

level: middleimportance: should knowfreq 42%

basics

~20 s

triggerEventHandler calls the listeners Angular registered on that one element directly, with whatever object you pass. A real event goes through the browser: it respects disabled controls, bubbles and runs default actions. That gap can make broken UI pass.

open as a page

Moving an Angular test suite from zone.js to zoneless, which ComponentFixture habits break or change meaning, and how do you rewrite them?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Zoneless fixtures always auto-detect, detectChanges refreshes only views Angular was notified about, and plain field assignments either stay stale on OnPush views or throw NG0100 on Eager ones. Rewrite tests to change state through signals, setInput and events, then await whenStable.

open as a page