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.
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.
Useful tests make behavior safe to change. They document contracts through meaningful assertions rather than implementation details or line coverage alone.
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. |
ng test
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 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.
ng test --no-watch --code-coverage
{
"test": {
"options": {
"codeCoverage": true
}
}
}
Vitest uses Jest-compatible syntax. Here is a complete example testing a service and a standalone component:
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);
});
});
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');
});
});
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.
ng new my-project
ng g @schematics/angular:refactor-jasmine-vitest
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. |
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. |
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.
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.
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. |
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.
Explore 500+ free tutorials across 20+ languages and frameworks.