skip to content

Unit Testing Toolkit

TestBed, component fixtures, fake clocks, HTTP and router doubles and CDK harnesses: the test APIs Angular ships. Interviewers probe whether your specs test behavior or fight the framework.

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

explore

questions

29

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 a new Angular CLI project, which builder and test runner does ng test use by default, and is Karma still supported?

level: juniorimportance: must knowfreq 52%

basics

~10 s

Since v21, new projects run ng test through the @angular/build:unit-test builder with Vitest in Node.js and jsdom; Karma with Jasmine is still supported, selectable with --test-runner=karma or the builder's runner option.

open as a page

In Angular, how do you unit-test a service that loads a user profile through HttpClient without sending a real network request?

level: juniorimportance: must knowfreq 66%

basics

~10 s

Add provideHttpClientTesting() to TestBed's providers, subscribe to the service call, then use HttpTestingController to expectOne the request, flush a canned body, assert the result, and call verify() so no unclaimed request remains.

open as a page

In Angular, how do you configure TestBed for a standalone component test, and what belongs in imports versus providers?

level: juniorimportance: must knowfreq 72%

basics

~10 s

Call TestBed.configureTestingModule with the standalone component and any modules it needs in imports and your service doubles in providers, then create it with TestBed.createComponent; a standalone component never goes in declarations.

open as a page

In a zone-based Angular test, how do you use fakeAsync and tick to prove a search input waits 300 ms after typing stops?

level: middleimportance: must knowfreq 55%

basics

~10 s

Wrap the test in fakeAsync, type with dispatchEvent, tick to just under 300 ms and assert nothing was searched, then tick the last millisecond, call detectChanges and assert the request and the rendered results.

open as a page

In a zoneless Angular test suite on Vitest, what replaces fakeAsync and tick, and how do you test a 300 ms debounced search?

level: middleimportance: must knowfreq 50%

basics

~20 s

The runner's fake timers replace fakeAsync: install them, advance virtual time with the runner's async advance calls, and remember that Angular's zoneless scheduler also queues renders on timers, so advance until the render runs before asserting.

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 Angular, when can you unit-test a service with a plain new instead of TestBed, and how do you swap its dependencies for doubles?

level: middleimportance: must knowfreq 57%

basics

~10 s

Plain new works when dependencies arrive as constructor parameters; a service using inject() needs an injection context, so use TestBed.inject() and replace dependencies with providers such as { provide: UserProfileApi, useValue: fake }.

open as a page

In Angular, why can a configureTestingModule fake not replace a Clock a component provides itself, and how do you swap it?

level: middleimportance: must knowfreq 58%

basics

~20 s

A component's own providers create Clock in its element injector, which is asked before the testing module, so the module-level fake is never reached. Replace it with TestBed.overrideComponent on the providers metadata or TestBed.overrideProvider(Clock, { useValue }).

open as a page

In Angular testing, what is the difference between waitForAsync and fakeAsync, and when would a test use each?

level: juniorimportance: should knowfreq 55%

basics

~10 s

waitForAsync lets real time pass and ends the test when every async task it started has finished; fakeAsync runs the test on a virtual clock you advance with tick or flush. Both need zone.js.

open as a page

In Angular, what is a CDK component harness, and how do you load one for a component in a TestBed unit test?

level: juniorimportance: should knowfreq 38%

basics

~10 s

A CDK component harness is a ComponentHarness subclass that drives a component through a typed async API; in a TestBed test you create the fixture, call TestbedHarnessEnvironment.loader(fixture), then await loader.getHarness(DropdownHarness).

open as a page

In an Angular test, why doesn't an effect() run right after a signal write, and what does TestBed.tick() do about it?

level: middleimportance: should knowfreq 36%

basics

~20 s

Angular effects run asynchronously as part of change detection, never inside the signal write. TestBed.tick() runs that pending work synchronously, flushing root effects and refreshing test views, so the test can assert the effect's result right away.

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

In an Angular CDK ComponentHarness, how do locatorFor, locatorForOptional and locatorForAll differ, and why do they return functions rather than elements?

level: middleimportance: should knowfreq 26%

basics

~20 s

locatorFor resolves to the first match and rejects if none, locatorForOptional resolves to the first match or null, locatorForAll resolves to an array; they return functions so each call queries the current DOM, never a stale element.

open as a page

With Angular's HttpTestingController, how do you test a user-profile service's handling of a 404 response versus a network failure?

level: middleimportance: should knowfreq 46%

basics

~10 s

Claim the request with expectOne, then answer a 404 with flush(body, { status: 404, statusText: 'Not Found' }) and a network failure with error(new ProgressEvent('error')); both surface as HttpErrorResponse, the second with status 0.

open as a page

In Angular's TestBed, what freezes the testing module, and in what order must configuration, overrides, compileComponents and inject run?

level: middleimportance: should knowfreq 45%

basics

~10 s

The first TestBed.inject, createComponent or runInInjectionContext instantiates the testing module, and later configure or override calls throw. So configure, then override, then await compileComponents if needed, then inject and create.

open as a page

In an Angular fakeAsync test, why can flush() leave an RxJS debounceTime unfired, and what do discardPeriodicTasks and the flush option change?

level: seniorimportance: should knowfreq 30%

basics

~20 s

RxJS's async scheduler, used by debounceTime, schedules with setInterval, and flush() stops once only periodic timers remain, so the debounce may never fire. Use tick(ms). discardPeriodicTasks drops leftover intervals; fakeAsync's default flush option flushes timers at the end.

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

How would you write an Angular CDK ComponentHarness for a custom dropdown so that consuming tests never depend on its internal DOM?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Extend ComponentHarness with hostSelector 'app-dropdown', keep locatorFor/locatorForOptional/locatorForAll protected, expose intent-level async methods like selectOption(label) that return plain values, and add a static with() returning a HarnessPredicate.

open as a page

An Angular test using a CDK dropdown harness cannot find the options shown in an overlay and never sees the loading state; why, and how do you fix both?

level: seniorimportance: should knowfreq 27%

basics

~10 s

The fixture loader and harness locators search below their root, but overlays attach to document.body, so use documentRootLocatorFactory() or documentRootLoader(fixture); harnesses auto-run change detection and wait for stability, so wrap transient-state checks in manualChangeDetection.

open as a page

How would you move an existing Angular Karma and Jasmine suite to the unit-test builder with Vitest, and what does the migrate-karma-to-vitest migration leave for you?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Ensure the build uses @angular/build:application, run the optional migrate-karma-to-vitest migration to switch the test target to @angular/build:unit-test with Vitest, then install jsdom, run refactor-jasmine-vitest on the specs, and port custom Karma config by hand.

open as a page

An Angular HttpClient test fails because HttpTestingController.expectOne reports 'found none' although the service method ran; what do you check, and why might verify() fail too?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Check the observable was subscribed, the matcher equals the full URL with query string, the request was not sent later or already claimed, and the real backend was not re-provided; verify() reports anything left open, cancelled requests included.

open as a page

An Angular suite passes spec by spec but fails in full runs, with services and DOM nodes surviving between tests; how does TestBed's teardown configuration bear on this?

level: seniorimportance: should knowfreq 36%

basics

~20 s

With teardown.destroyAfterEach true, the default, TestBed destroys fixtures and the testing module after each test, running service ngOnDestroy hooks and removing host elements. If it is false, or state lives outside DI, it leaks into the next test.

open as a page

In Angular CDK testing, how do you pick one dropdown among several on a page using HarnessLoader queries and a HarnessPredicate?

level: middleimportance: nice to knowfreq 20%

basics

~10 s

Pass a HarnessPredicate instead of the bare harness class: DropdownHarness.with({ selector: '[name="country"]' }) or an ancestor filter narrows the matches, because getHarness otherwise returns whichever matching dropdown comes first.

open as a page

With Angular's unit-test builder, what changes when specs run in jsdom versus a real browser configured through the browsers option?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

By default specs run in Node.js with jsdom (or happy-dom): fast, but no layout or real browser behaviour. Setting browsers runs them in a real browser through a provider like @vitest/browser-playwright: slower, with real layout, CSS and events.

open as a page

What does the Angular CLI's refactor-jasmine-vitest schematic rewrite in spec files, and what must you still check or fix by hand?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

refactor-jasmine-vitest rewrites Jasmine APIs in spec files to Vitest (fit to it.only, spyOn to vi.spyOn, createSpy to vi.fn and more) and leaves TODOs; it installs nothing, changes no angular.json, and cannot handle every complex spy.

open as a page

In Angular, what does RouterTestingHarness give a routed-component test, and how do its navigateByUrl and routeNativeElement behave?

level: middleimportance: nice to knowfreq 28%

basics

~10 s

RouterTestingHarness runs the real router in a unit test: with provideRouter(routes) configured, await navigateByUrl(url) completes the navigation, runs change detection and returns the activated component, while routeNativeElement exposes that component's element.

open as a page

Why does an Angular TestBed test fail on an error the running app would only log, and what does rethrowApplicationErrors control?

level: middleimportance: nice to knowfreq 27%

basics

~20 s

TestBed reports errors Angular catches during change detection and event listeners to ErrorHandler and then rethrows them, so they fail the test instead of just logging. rethrowApplicationErrors, true by default, turns that rethrow off for one test.

open as a page