Testing Angular Apps
TestBed, fixture.detectChanges(), and complete component test examples.
TestBed: Angular's testing environment
Angular components depend heavily on the framework's own machinery — dependency injection, templates, lifecycle hooks — so testing one in isolation, the way you might instantiate a plain JavaScript class directly, generally doesn't work. TestBed is Angular's testing utility for configuring a small, real Angular environment around the thing under test — wiring up dependency injection, compiling the component's template, and giving you a handle to interact with the rendered result, all inside a test.
Angular CLI projects come with a test runner (traditionally Karma, though Jest and the newer Vitest-based builder are both increasingly common) already configured, plus a ComponentName.spec.ts file automatically generated alongside every component ng generate creates.
ng generate component counter
# creates counter.component.ts, counter.component.html, and counter.component.spec.ts
A complete example: testing a counter component
The component under test:
// counter.component.ts
import { Component } from "@angular/core";
@Component({
selector: "app-counter",
standalone: true,
template: `
<p>Count: {{ count }}</p>
<button (click)="increment()">Increment</button>
<button (click)="reset()">Reset</button>
`,
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
reset() {
this.count = 0;
}
}
Its test:
// counter.component.spec.ts
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { CounterComponent } from "./counter.component";
describe("CounterComponent", () => {
let fixture: ComponentFixture<CounterComponent>;
let component: CounterComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent], // a standalone component is imported directly, not declared
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges(); // triggers the initial render
});
it("starts at zero", () => {
const p = fixture.nativeElement.querySelector("p");
expect(p.textContent).toContain("Count: 0");
});
it("increments when the Increment button is clicked", () => {
const buttons = fixture.debugElement.queryAll(By.css("button"));
buttons[0].nativeElement.click(); // Increment
buttons[0].nativeElement.click();
fixture.detectChanges(); // re-render after the state change
const p = fixture.nativeElement.querySelector("p");
expect(p.textContent).toContain("Count: 2");
});
it("resets back to zero", () => {
component.count = 5;
fixture.detectChanges();
const buttons = fixture.debugElement.queryAll(By.css("button"));
buttons[1].nativeElement.click(); // Reset
fixture.detectChanges();
expect(component.count).toBe(0);
});
});
A few pieces worth naming individually: TestBed.configureTestingModule({ imports: [CounterComponent] }) sets up a minimal Angular module for the test — a standalone component goes in imports (it declares its own dependencies already), while an older, non-standalone component would instead go in declarations. fixture.componentInstance is the actual CounterComponent instance, letting a test read or set its properties directly when that's simpler than interacting through the DOM. fixture.detectChanges() is the detail most new Angular tests trip over: Angular does not automatically re-run change detection inside a test the way it does in a running app, so a state change made either through a simulated click or by setting a component property directly won't be reflected in the DOM until detectChanges() is called again.
Testing a component with a mocked dependency
A component that depends on a service (via Angular's dependency injection, covered on its own page in this track) is tested by providing a fake implementation of that service instead of the real one — the same substitution shown in the services-and-DI page's own testability example, applied to a full component test:
// user-list.component.ts
import { Component, inject } from "@angular/core";
import { UserService } from "./user.service";
@Component({
selector: "app-user-list",
standalone: true,
template: `
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
`,
})
export class UserListComponent {
private userService = inject(UserService);
users = this.userService.getUsers();
}
// user-list.component.spec.ts
import { TestBed } from "@angular/core/testing";
import { UserService } from "./user.service";
import { UserListComponent } from "./user-list.component";
describe("UserListComponent", () => {
it("renders each user returned by UserService", async () => {
const fakeUserService = {
getUsers: () => [
{ id: 1, name: "Test User One" },
{ id: 2, name: "Test User Two" },
],
};
await TestBed.configureTestingModule({
imports: [UserListComponent],
providers: [{ provide: UserService, useValue: fakeUserService }],
}).compileComponents();
const fixture = TestBed.createComponent(UserListComponent);
fixture.detectChanges();
const items = fixture.nativeElement.querySelectorAll("li");
expect(items.length).toBe(2);
expect(items[0].textContent).toContain("Test User One");
});
});
providers: [{ provide: UserService, useValue: fakeUserService }] tells Angular's injector "whenever something asks for UserService, hand it this plain object instead" — UserListComponent never knows the difference, since it only ever depends on UserService's public shape (getUsers()), not a specific implementation. This is the direct practical payoff of programming to dependency injection in the first place: a real service that would otherwise make an HTTP call is swapped out for a synchronous, predictable fake with zero changes to the component under test.
Common mistakes
- Forgetting to call
fixture.detectChanges()after a state change (whether from a simulated user interaction or a direct property assignment) — Angular doesn't run change detection automatically inside a test, so the DOM silently stays stale until it's called again. - Adding a standalone component to
declarationsinstead ofimportsinTestBed.configureTestingModule— standalone components declare their own dependencies and are imported like any other importable unit;declarationsis for the older, non-standalone component style. - Testing a component by directly calling its class methods (
component.increment()) for everything, without ever asserting on what actually renders in the DOM — this can pass even when the template itself is broken, since it never touches the rendered output at all. - Instantiating a real service (making an actual HTTP call) inside a component test instead of providing a fake with
useValue/useClass— this makes the test slow, flaky, and dependent on a real backend being reachable, for what should be an isolated unit test of the component alone.