Angular – Testing

August 8, 20263 min readUpdated 8/21/2026

Angular projects ship with a unit test runner and, usually, an end-to-end one alongside. The pizza app uses Vitest for units and Playwright for flows through the UI. What follows is the useful half: what to test where, and the mechanics that are not obvious.

The cheapest correct test

describe('MoneyPipe', () => {
  const pipe = new MoneyPipe();

  it('formats a number as US currency', () => {
    expect(pipe.transform(14.5)).toBe('$14.50');
  });

A pipe is a class with one method. Nothing about it needs Angular running, so new MoneyPipe() and a call is the whole test — milliseconds rather than tens of them.

Reach for TestBed when you need Angular's injector or its rendering, and not before. Most services and every pure pipe need neither.

TestBed

TestBed.configureTestingModule({
  providers: [
    provideRouter([]),
    provideHttpClient(),
    provideHttpClientTesting(),
    { provide: MenuService, useValue: menuServiceStub([PEPPERONI, PEPSI]) },
  ],
});

TestBed builds a miniature application: an injector you configure, and a place to create components. Because dependencies arrive through DI, swapping a real service for a stub is one provider — no mocking library, no module interception.

⚠️ Order matters. provideHttpClientTesting() must come after provideHttpClient(), because it replaces the backend the first installed. Reversed, the real backend wins and your tests hit the network.

Testing HTTP

it('attaches the bearer token when one is stored', () => {
  tokenStore.set('a-test-token');

  http.get('/api/me/orders').subscribe();

  const req = httpMock.expectOne(`${environment.apiBaseUrl}/api/me/orders`);
  expect(req.request.headers.get('Authorization')).toBe('Bearer a-test-token');
});

HttpTestingController hands you every request as an object to assert on, which is what makes an interceptor — otherwise invisible plumbing — directly testable.

afterEach(() => {
  httpMock.verify();

verify() is the part people leave out and then regret. It fails the test if a request was made that no expectation consumed, which is how you discover the component firing a second request you never knew about.

Testing a directive

@Component({
  imports: [Autofocus],
  template: `
    <input id="plain" />
    <input id="focused" appAutofocus />
  `,
})
class EnabledHost {}

A directive has no template of its own, so there is nothing to instantiate directly. A throwaway host component in the spec file uses it the way a real template would — which also exercises the selector. A directive whose selector is misspelled still passes a test that news up the class by hand; it does not pass this one.

const fixture = TestBed.createComponent(EnabledHost);
fixture.detectChanges();
await fixture.whenStable();

await fixture.whenStable() is load-bearing here: the directive focuses inside afterNextRender, so asserting immediately after createComponent checks before it has happened and fails for the wrong reason.

Testing a functional guard

return TestBed.runInInjectionContext(() =>

A functional guard is just a function, but it may call inject() — so it has to run inside an injection context. That is the whole trick, and it is why a guard cannot simply be called like an ordinary function in a test.

Testing time

The search pipeline debounces for 300 ms. Waiting it out in every assertion would make the suite slow enough that people stop running it:

vi.useFakeTimers();
expect(first.cancelled).toBe(true);
expect(second.cancelled).toBe(false);

And req.cancelled is the only direct evidence that switchMap unsubscribed rather than letting both requests run. Asserting on the final rendered results cannot distinguish "cancelled the stale request" from "got lucky with the ordering" — the test would pass either way, which makes it worthless for the thing it exists to check.

Two gotchas paid for already

A stub must cover the whole component tree, not just the component. The menu page's spec stubs MenuService, but its template renders the pizza builder, which injects CartService, which reads menu.crusts() in an effect. A four-member stub failed inside change detection with this.menu.crusts is not a function — a stack trace pointing at CartService for a spec about the menu page.

Fixtures need their real shape. ProductCard maps over product.sizes; a trimmed object literal fails deep inside a child template rather than in the spec that wrote it.

Where end-to-end takes over

Unit tests answer "does this piece behave". They cannot answer "can somebody actually order a pizza". The Playwright suite drives every flow through the real UI against the real backend — browse, cart, auth, checkout, orders, profile, admin — plus the API guards that are not observable through the UI at all.

It runs serially, because it is integration testing against one backend and one database, and tests must clean up what they create or one failure poisons every later run.

What is next

Why an Angular app gets slow, and what change detection is actually doing.