Tutorials Logic, IN info@tutorialslogic.com

Angular Unit Testing with Vitest and TestBed

Testing Foundations

New Angular CLI projects use Vitest and jsdom by default. Learn focused service and component tests, TestBed configuration for standalone components, coverage as a diagnostic metric, and when browser-mode testing is required.

Testing Scope

A focused test proves one observable behavior through the smallest useful public boundary. New Angular CLI projects use Vitest with jsdom by default; existing workspaces may retain another configured runner.

Test component DOM and events through ComponentFixture, test injectable logic through TestBed or direct construction, and use dedicated testing providers for HTTP and routing instead of calling real infrastructure.

Testing Benefits

Useful tests make behavior safe to change. They document contracts through meaningful assertions rather than implementation details or line coverage alone.

  • Protect state transitions, rendered output, emitted events, request contracts, and failure behavior.
  • Reveal tightly coupled designs when dependencies cannot be replaced cleanly.
  • Support refactoring by asserting public behavior instead of private methods and internal fields.
  • Keep each failure diagnostic by arranging one behavior, performing one action, and asserting the relevant result.

Test Setup and Coverage

Test Configuration

The Angular CLI configures the workspace test target. Run ng test from the workspace root, and inspect angular.json before assuming an existing project uses the current default runner.

The default jsdom environment is fast and suitable for most component logic and DOM assertions. Use browser mode or an end-to-end tool when layout, browser-only APIs, rendering engines, or cross-page journeys are part of the behavior.

Command or API Purpose
ng test Runs the configured test target in development watch mode when supported.
ng test --no-watch Runs the suite once for continuous integration.
ng test --code-coverage Collects the configured line, branch, function, and statement coverage.
TestBed.configureTestingModule(...) Builds an Angular test injector and compilation scope.
TestBed.createComponent(Type) Creates a ComponentFixture for class and rendered-DOM testing.

Test Configuration Example

Test Configuration Example
ng test

Vitest Output

Vitest Output
DEV  v1.x.x  /my-angular-app

 ✓ src/app/app.component.spec.ts (3 tests) 12ms
 ✓ src/app/counter.service.spec.ts (2 tests) 5ms

 Test Files  2 passed (2)
      Tests  5 passed (5)
   Start at  10:23:45
   Duration  1.23s

Coverage Reports

Coverage reports show which statements, branches, functions, and lines were executed by the test suite. Run coverage in a non-watch CI command, then inspect uncovered behavior rather than treating a percentage as proof of correctness.

The test target writes an HTML report under the configured coverage directory. Open its index page to trace an uncovered branch back to the source, then add a behavior-focused test only when that branch represents a meaningful contract.

Set codeCoverage in the test target only when every local run should collect it; coverage instrumentation makes the feedback loop slower.

Coverage Reports Example

Coverage Reports Example
ng test --no-watch --code-coverage

Coverage Reports Setup

Coverage Reports Setup
{
  "test": {
    "options": {
      "codeCoverage": true
    }
  }
}

Testing with Vitest

Vitest Component Tests

Vitest uses Jest-compatible syntax. Here is a complete example testing a service and a standalone component:

Vitest Tests

Vitest Tests
import { describe, it, expect, beforeEach } from 'vitest';
import { CounterService } from './counter.service';

describe('CounterService', () => {
  let service: CounterService;

  beforeEach(() => {
    service = new CounterService();
  });

  it('should start at 0', () => {
    expect(service.count()).toBe(0);
  });

  it('should increment', () => {
    service.increment();
    expect(service.count()).toBe(1);
  });

  it('should reset', () => {
    service.increment();
    service.reset();
    expect(service.count()).toBe(0);
  });
});

Standalone Component Test with Vitest

Standalone Component Test with Vitest
import { describe, it, expect } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  it('should create the component', async () => {
    await TestBed.configureTestingModule({
      imports: [AppComponent]
    }).compileComponents();

    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });

  it('should render title', async () => {
    await TestBed.configureTestingModule({
      imports: [AppComponent]
    }).compileComponents();

    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const el = fixture.nativeElement as HTMLElement;
    expect(el.querySelector('h1')?.textContent).toContain('Angular');
  });
});

Existing Test Suites and Migration

New Angular CLI projects use Vitest by default. Existing workspaces may continue using Karma or another configured runner, so inspect the test target before changing imports, timers, browser assumptions, or CI commands.

A new workspace receives the current default test setup through ng new. Do not add a second runner unless the project has a specific compatibility need.

For an existing Jasmine suite, run the Angular migration in a reviewable branch, then inspect custom matchers, spies, fake timers, globals, and browser-only APIs before removing the old runner.

  • Keep runner-specific imports consistent; do not mix Jasmine globals and Vitest helpers accidentally.
  • Review fakeAsync, fake timers, and clock-dependent tests instead of assuming identical scheduling.
  • Use jsdom for fast DOM contracts and browser mode when the real rendering engine matters.
  • Run the complete suite and compare CI coverage before deleting the previous configuration.

Vitest Migration Example

Vitest Migration Example
ng new my-project

Vitest Migration Setup

Vitest Migration Setup
ng g @schematics/angular:refactor-jasmine-vitest

TestBed Patterns

TestBed creates an Angular injector and component compilation scope for a test. Configure only the dependencies needed by the subject, then assert behavior through the class API, rendered DOM, outputs, or collaborator calls.

Pattern Purpose
imports: [StandaloneComponent] Makes a standalone component and its metadata available to the test.
providers: [{ provide: Api, useValue: apiStub }] Replaces a collaborator with a controlled test double.
fixture.componentRef.setInput('name', 'Ada') Assigns an input through Angular so input lifecycle behavior is preserved.
fixture.detectChanges() Runs binding and updates the fixture DOM for the current state.
await fixture.whenStable() Waits until Angular reports pending framework work has stabilized.
fixture.nativeElement.querySelector(...) Asserts the DOM observable to a user without coupling to private fields.
outputRef.subscribe(handler) Observes a programmatic component output and verifies the emitted contract.
  • Construct a plain class directly when no Angular injection, template, signals tied to a context, or framework metadata is involved.
  • Use TestBed.inject for injectable services whose provider configuration is part of the test.
  • Do not call private methods merely to increase coverage; trigger the public action that should reach them.

Async Tests

Match the test control to the asynchronous source. Await promises and fixture stabilization, use the runner's fake timers for timers you own, and flush controlled HTTP requests through HttpTestingController.

Async source Test control
Promise or async method Make the test async and await the returned promise.
Angular stabilization Use await fixture.whenStable(), then run detectChanges before reading updated DOM.
setTimeout or interval Use Vitest fake timers and advance only the duration relevant to the behavior.
Observable Supply a deterministic of(...) or throwError(...) source, or assert through the final consumer.
HTTP request Expect the request, assert its method/body/headers, then flush a response or error.

HTTP Testing

HttpTestingController captures requests made through HttpClient so a unit test can assert the transport contract without reaching a network. Register provideHttpClient before provideHttpClientTesting because the testing provider overrides parts of the client configuration.

  • Call expectOne with the exact request matcher expected for this behavior.
  • Assert method, URL, parameters, headers, or body before supplying the response.
  • Use flush for successful and backend-error responses; use error for a network-level failure.
  • Call verify after each test so unexpected or unflushed requests fail the suite.

Service Request Contract

Service Request Contract
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import {
  HttpTestingController,
  provideHttpClientTesting
} from '@angular/common/http/testing';
import { UserApi } from './user-api';

describe('UserApi', () => {
  let api: UserApi;
  let http: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        UserApi,
        provideHttpClient(),
        provideHttpClientTesting()
      ]
    });
    api = TestBed.inject(UserApi);
    http = TestBed.inject(HttpTestingController);
  });

  afterEach(() => http.verify());

  it('loads one user', () => {
    let name = '';
    api.getUser(7).subscribe(user => name = user.name);

    const request = http.expectOne('/api/users/7');
    expect(request.request.method).toBe('GET');
    request.flush({ id: 7, name: 'Ada' });

    expect(name).toBe('Ada');
  });
});

The test proves both the outgoing GET contract and the value delivered to the subscriber. verify catches any extra request the test forgot to handle.

Testing Layers

A unit test is not the right instrument for every risk. Keep most logic and component-contract checks fast, then add fewer browser or end-to-end tests for behavior that depends on the real rendering engine, navigation, storage, accessibility tree, or backend integration.

Layer Best evidence
Plain unit test Pure calculation, reducer, validator, formatter, or isolated service behavior.
TestBed component test Bindings, inputs, outputs, providers, conditional DOM, and user events.
HTTP/router test provider Request or navigation contract without real infrastructure.
Browser mode Browser-specific DOM APIs or rendering behavior that jsdom does not model.
End-to-end test Critical user journey across routes, authentication, network, and deployed configuration.
Confirm the page outcome

Test Review

5 checks
  • I can choose direct construction, TestBed, HTTP testing, browser mode, or end-to-end testing from the risk being tested.
  • I can configure a standalone component and replace its providers without calling real infrastructure.
  • I can update inputs, run change detection, and assert rendered DOM or emitted outputs.
  • I can control promises, timers, Observables, and HTTP requests deterministically.
  • I use coverage to find untested paths, not as proof that assertions are meaningful.

Test Review Questions

Imports. A standalone component already carries Angular metadata and its template dependencies. declarations is for non-standalone declarables in an NgModule-style test.

Changing class state and rendering the view are separate steps in a unit test. The component instance may contain the new title while the fixture DOM still reflects the previous change-detection pass.

Coverage reports which lines and branches executed, not whether the assertions proved meaningful behavior. A test can call a method, execute every line, and never verify the emitted state, rendered result, request body, or failure path.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.