Angular Testing: A Practical Guide
Learn Angular’s current testing setup with Vitest, TestBed, HTTP mocks, coverage, browser mode, CI commands, and migration guidance for Karma projects.
For new Angular CLI projects, run ng test: the CLI sets up Vitest with jsdom for unit tests. Use browser mode when browser-specific APIs, rendering behavior, or real-browser debugging matter. Existing Karma projects remain supported; migrating them to Vitest is experimental.
This guide covers the default setup, practical service, component, and HTTP tests, coverage, CI, browser mode, and Karma migration. The commands and configuration below follow Angular’s current documentation; check the linked docs for version-sensitive builder and provider details before changing an older project.
1. Choose the test environment
| Situation | Recommended starting point | Why |
|---|---|---|
| New CLI project or ordinary unit tests | Vitest with jsdom, the CLI default | Runs in Node.js without launching a browser; Angular describes this as faster for most unit tests. |
| Code depends on browser APIs or browser rendering | Vitest browser mode with a browser provider | Exercises tests in an actual browser environment and supports browser-focused debugging. |
| Existing Karma/Jasmine project | Keep Karma, or evaluate the experimental migration | Karma remains supported. Migration may involve builder options and custom configuration. |
DOM emulation is useful for most isolated tests, but it is not a complete browser. If a test depends on browser rendering or an API the emulator does not implement, move that coverage to browser mode rather than assuming the emulation proves browser behavior.
2. Run the Angular test suite
From the project root, run:
ng test
In interactive use, the test target watches files. In CI, set CI=true to use non-interactive single-run behavior. If your CI system does not set that variable, use:
ng test --no-watch --no-progress
Tests are commonly stored beside the code they cover in *.spec.ts files. Angular’s test target can be configured in angular.json. The documented configuration covers file inclusion and exclusion, setup files, provider files, coverage, browser selection, and an optional custom runner configuration. Angular handles most Vitest configuration itself. Custom runner files are advanced: Angular does not support their contents or third-party plugins.
To inspect the current test target and its builder, look in the project’s angular.json rather than assuming every project was generated with today’s defaults. Older projects may use a Karma builder even though new CLI projects use Vitest.
3. Test a service with TestBed
TestBed creates an isolated Angular testing environment and lets the test retrieve an instance through dependency injection. A small service with no external dependencies can be tested directly:
// greeting.service.ts
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class GreetingService {
greeting(name: string): string {
return `Hello, ${name}`;
}
}
// greeting.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { GreetingService } from './greeting.service';
describe('GreetingService', () => {
let service: GreetingService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(GreetingService);
});
it('greets the supplied name', () => {
expect(service.greeting('Ada')).toBe('Hello, Ada');
});
});
By default, a service’s real dependencies are used. When a dependency would make the test slow, nondeterministic, or coupled to an external system, provide a controlled alternative in the test module configuration. Keep the service’s business logic under test while replacing only the dependency behavior the test does not need.
4. Test a component’s rendered behavior
A component test should cover the relationship between the class and template: configure the testing module, create a fixture, trigger change detection, and inspect the rendered result. For example:
// welcome.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-welcome',
standalone: true,
template: '<h1>Welcome, {{ name }}</h1>',
})
export class WelcomeComponent {
name = 'Ada';
}
// welcome.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { WelcomeComponent } from './welcome.component';
describe('WelcomeComponent', () => {
it('renders the current name', async () => {
const fixture = TestBed.createComponent(WelcomeComponent);
fixture.detectChanges();
const heading = fixture.nativeElement.querySelector('h1');
expect(heading.textContent).toContain('Welcome, Ada');
});
});
ComponentFixture gives the test control over the component instance, change detection, and rendered view. Angular’s DebugElement provides a platform-aware way to inspect elements and bindings. Direct access through nativeElement is convenient when the DOM implementation provides the APIs you use; prefer Angular’s abstraction when portability across environments matters.
For interaction tests, change the component state or dispatch the relevant event through the fixture’s element, run change detection, and assert the user-visible result. Avoid testing only private implementation details when the actual contract is what appears or changes in the template.
5. Test HTTP requests without a real backend
Angular’s HTTP testing utilities capture outgoing requests so tests can assert their method and URL and provide controlled responses. This keeps unit tests independent of a live backend.
// user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly http = inject(HttpClient);
getUser(id: number) {
return this.http.get<{ id: number; name: string }>(`/api/users/${id}`);
}
}
// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
let http: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});
service = TestBed.inject(UserService);
http = TestBed.inject(HttpTestingController);
});
afterEach(() => http.verify());
it('requests a user and returns the controlled response', () => {
let result: { id: number; name: string } | undefined;
service.getUser(42).subscribe(value => result = value);
const request = http.expectOne('/api/users/42');
expect(request.request.method).toBe('GET');
request.flush({ id: 42, name: 'Ada' });
expect(result).toEqual({ id: 42, name: 'Ada' });
});
});
Configure provideHttpClientTesting() after provideHttpClient(), make assertions against the captured request, and flush a response. Verify outstanding requests after each test. If a request is expected to fail, flush an error response and assert the error handling path; do not leave the request unflushed.
6. Measure code coverage
For Vitest coverage, install the V8 provider and run the CLI coverage option:
npm install --save-dev @vitest/coverage-v8
ng test --coverage
Angular documents the report output in the coverage/ directory. Coverage tells you which code was executed; it does not establish that assertions check meaningful behavior. Use the report to find untested branches and important behaviors, then write assertions that would fail if those behaviors broke.
7. Use browser mode when the test needs a browser
Browser mode is appropriate for tests that depend on browser-specific APIs, rendering, or browser debugging. Angular documents Playwright and WebdriverIO providers. Install the provider and browser dependencies specified by the relevant Angular documentation, then configure the test target’s browsers option. Provider setup and option names depend on the selected integration, so use the current provider instructions rather than copying a configuration for another provider.
For CI, Angular uses headless mode automatically when the CI environment variable is set. A browser name can also explicitly select headless mode. Browser execution adds setup and runtime overhead compared with Node.js plus DOM emulation, so reserve it for cases where browser fidelity provides value.
8. Migrate an existing Karma project carefully
Angular supports Karma, so migration is a choice rather than an urgent repair. Angular labels migration to Vitest experimental and requires the application build system.
- Review the current builder in
angular.jsonand inventory customkarma.conf.jsbehavior, test-specific build settings, plugins, and helper usage. - Follow Angular’s migration guide to add Vitest and a DOM emulator and change the test builder to
@angular/build:unit-test. - Move or revise test build options as required. The new builder does not accept every old Karma builder option in the same place.
- Use the refactoring schematic only as a starting point for common Jasmine patterns. It does not install dependencies, change the builder, move build options, remove old files, or transform complex and nested spy scenarios.
- Review every generated change, resolve unsupported custom setup, then run the suite and compare its intended coverage before removing old configuration.
Some Zone-based helpers can be patched during migration. Angular recommends planning a move toward native async code and Vitest fake timers. A successful schematic run is not proof that all custom test behavior migrated correctly.
9. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
ng test watches forever in CI |
The environment does not set CI. |
Set CI=true, or pass --no-watch --no-progress. |
| A browser API is missing in a test | The test runs under jsdom or another DOM emulator that lacks that browser behavior. | Use a suitable mock for a unit test or move browser-dependent coverage to browser mode. |
Component query returns null or rendered content is stale |
The fixture has not run change detection, or the selector does not match the template. | Call fixture.detectChanges() after setting state and check the template and selector. |
| HTTP test reports unmatched or outstanding requests | The request was not captured, flushed, or verified consistently. | Match the actual URL, flush every captured request, and use http.verify() after the test. |
| Coverage command cannot load the provider | @vitest/coverage-v8 is missing. |
Install it as a development dependency, then rerun ng test --coverage. |
| Migration has unknown builder options or plugin errors | Legacy Karma settings or unsupported custom Vitest configuration were carried over. | Audit the test target and custom config against the migration guide; Angular does not support arbitrary custom runner file contents or third-party plugins. |
| Tests fail only after conversion from Zone helpers | The suite still relies on timing or Zone-specific behavior not handled by the schematic. | Inspect converted async and timer tests, then migrate deliberately to native async patterns and Vitest fake timers. |
10. Performance, reliability, and cost
Node.js with jsdom or happy-dom avoids browser launch overhead and is Angular’s faster default path for most unit tests. Keep isolated logic and ordinary component behavior there where the emulated APIs suffice; use browser mode for the cases that need browser fidelity. In CI, use non-watch execution so the job exits after the suite.
Reliability comes from controlling dependencies and time: mock backend responses with Angular’s HTTP testing backend, avoid dependence on live services in unit tests, and explicitly flush and verify requests. For migration, account for custom Karma configuration and manually review schematic output. Neither a green suite nor a high coverage percentage guarantees correctness, so tests should assert observable behavior and important failure paths.
Angular testing is based on open-source development tools and local test execution; the research sources do not establish a per-test service fee. Account for the time and CI resources needed for browser setup and suite execution in your own environment. No performance benchmark is claimed here.
11. Capture screenshots of a running application
Angular unit tests verify code and component behavior; they are different from capturing a page image for documentation, review, or visual comparison. If you need a screenshot of a running Angular route, a browser automation setup can navigate to it and capture the page. That approach requires browser setup and may capture cookie banners, newsletter popups, or chat widgets as they appear to the browser.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners like a visitor, then removes 60+ known consent platforms, newsletter popups, and chat widgets before the shot; those steps can each be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for request options. Its MCP server provides take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Try it with the free ScreenshotNeo signup.
12. FAQ
Does a new Angular project use Karma?
Angular’s current CLI documentation describes Vitest as the default for new projects. Existing Karma projects remain supported.
Should every component test run in a browser?
No. DOM emulation suits many unit tests; use browser mode when a test relies on actual browser behavior or rendering.
Does coverage prove the suite is good?
No. It reports executed code, not whether assertions would catch regressions.
Is migrating from Karma automatic?
No. The migration is experimental, and custom configuration, builder options, and complex Jasmine patterns need review.


