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.

Bash
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:

Typescript
// 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:

Typescript
// 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:

Typescript
// 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();
}
Typescript
// 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 declarations instead of imports in TestBed.configureTestingModule — standalone components declare their own dependencies and are imported like any other importable unit; declarations is 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.