Testing
|
This section documents modern, standalone Angular — signals, the built-in This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted. This section’s bibliography lists the reference material consulted while preparing these pages. |
TestBed builds a miniature module around the unit under test, and ComponentFixture gives access to the
instance and its rendered DOM. The default runner is Karma with Jasmine; an official migration to Vitest is
under way. See Testing.
Anatomy of a component test
TestBed.configureTestingModule() declares the standalone components, imports, and providers the test needs;
TestBed.createComponent() returns a ComponentFixture.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Counter } from './counter';
describe('Counter', () => {
let fixture: ComponentFixture<Counter>;
let component: Counter;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Counter], // standalone component
}).compileComponents();
fixture = TestBed.createComponent(Counter);
component = fixture.componentInstance;
fixture.detectChanges(); // run the first change-detection pass
});
it('increments when the button is clicked', () => {
const button = fixture.debugElement.query(By.css('button'));
button.triggerEventHandler('click');
fixture.detectChanges();
expect(component.count()).toBe(1);
expect(fixture.nativeElement.textContent).toContain('Count: 1');
});
});
-
fixture.detectChanges()runs change detection — call it after every state change you want reflected in the DOM;fixture.autoDetectChanges()does it for you. -
fixture.componentInstanceis the class instance;fixture.nativeElementis the host DOM node. -
fixture.debugElementwraps the rendered tree —query(By.css(…)),queryAll(By.css(…)), andBy.directive(…)find nodes without touching the real DOM API.
Components with inputs and outputs
fixture.componentRef.setInput() sets a signal or decorator input; subscribe to an output() like an
observable.
it('emits selected on click', () => {
const fixture = TestBed.createComponent(ProductCard);
fixture.componentRef.setInput('product', { id: 7, name: 'Widget' });
fixture.detectChanges();
const selected = jasmine.createSpy('selected');
fixture.componentInstance.selected.subscribe(selected);
fixture.debugElement.query(By.css('button')).triggerEventHandler('click');
expect(selected).toHaveBeenCalledWith(7);
});
Replace a dependency with a stub through the providers array, and spy on its methods:
class FakeCart { add = jasmine.createSpy('add'); }
TestBed.configureTestingModule({
imports: [ProductCard],
providers: [{ provide: CartService, useClass: FakeCart }],
});
jasmine.createSpy() makes a standalone spy; spyOn(obj, 'method').and.returnValue(…) wraps an existing
method. See Component testing scenarios.
Services and HTTP
Test a plain service by pulling it from the injector:
TestBed.configureTestingModule({ providers: [PriceService] });
const service = TestBed.inject(PriceService);
expect(service.withTax(100)).toBe(121);
For services that call HttpClient, combine provideHttpClient() with provideHttpClientTesting() and drive
requests through HttpTestingController:
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
let http: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ProductApi, provideHttpClient(), provideHttpClientTesting()],
});
http = TestBed.inject(HttpTestingController);
});
afterEach(() => http.verify()); // fail if any request is still outstanding
it('fetches a product', () => {
const api = TestBed.inject(ProductApi);
let result: Product | undefined;
api.get(7).subscribe((p) => (result = p));
const req = http.expectOne('/api/products/7');
expect(req.request.method).toBe('GET');
req.flush({ id: 7, name: 'Widget' }); // resolve the request
expect(result?.name).toBe('Widget');
});
See Testing services and HTTP testing.
Asynchronous testing
-
fakeAsync+tick(ms)/flush()— run timers and microtasks synchronously on a virtual clock.tick(1000)advances time;flush()drains every pending timer. -
waitForAsync+fixture.whenStable()— wait for real promises and XHR to settle, then assert.
import { fakeAsync, tick } from '@angular/core/testing';
it('debounces the search', fakeAsync(() => {
component.search('ab');
component.search('abc');
tick(300);
expect(component.results()).toHaveSize(1);
}));
Pipes and attribute directives
A pipe is a plain class — call transform directly:
expect(new TruncatePipe().transform('a long string', 6)).toBe('a long…');
Test an attribute directive by applying it to a small host component and asserting the effect on the host element:
@Component({ imports: [HighlightDirective], template: `<p appHighlight="gold">Hi</p>` })
class Host {}
const fixture = TestBed.createComponent(Host);
fixture.detectChanges();
const p = fixture.debugElement.query(By.css('p'));
expect(p.styles['background-color']).toBe('gold');
Testing the router
Provide a router with provideRouter([…]) and navigate with RouterTestingHarness. Run a functional guard
or resolver inside TestBed.runInInjectionContext(…) so inject() resolves.
import { provideRouter } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
TestBed.configureTestingModule({
providers: [provideRouter([{ path: 'products/:id', component: ProductPage }])],
});
const harness = await RouterTestingHarness.create();
const page = await harness.navigateByUrl('/products/7', ProductPage);
expect(page.id()).toBe(7);
// A functional guard in isolation:
const allowed = TestBed.runInInjectionContext(() => authGuard(routeSnapshot, stateSnapshot));
expect(allowed).toBe(true);
Component harnesses
@angular/cdk/testing harnesses give a stable, implementation-independent API for driving a component in
tests. Load them through a HarnessLoader from TestbedHarnessEnvironment; Angular Material ships a harness
per component.
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
const loader: HarnessLoader = TestbedHarnessEnvironment.loader(fixture);
const button = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
await button.click();
Coverage and the Vitest migration
Run ng test --code-coverage to emit an Istanbul report under coverage/; enforce thresholds in
karma.conf.js (coverageReporter.check). Treat coverage as a gap finder, not a target. See
Code coverage.
Angular is moving the default runner from Karma/Jasmine (now deprecated) to Vitest. New workspaces can
opt in with the @angular/build:unit-test builder; existing ones follow
the Vitest migration guide. TestBed,
ComponentFixture, and component harnesses are unchanged — only the runner and its assertion globals differ
(vi in place of jasmine, Jest-style matchers).