Test Angular Components and Services with Vitest

Angular Tutorial: Beginner to Advanced · 11. Testing, accessibility and performance · 3 min read · Updated

Tests are small programs that check that your code still works after you change it. New Angular projects use Vitest as the default test runner, with the Angular TestBed to create components. Older tutorials use Karma and Jasmine, and the test code looks almost the same.

What you will learn

  • How to run tests with ng test
  • How to test a component by clicking and checking the text
  • How to test an HTTP service without a real server

Run the tests

ng test

This watches your files and re-runs tests when they change. In scripts and on a build server use ng test --watch=false. Test files end in .spec.ts and sit next to the code they check. describe, it and expect are available without imports.

Test the counter component

// src/app/counter/counter.spec.ts
import { TestBed } from '@angular/core/testing';
import { Counter } from './counter';

describe('Counter', () => {
  function setup() {
    const fixture = TestBed.createComponent(Counter);
    const element = fixture.nativeElement as HTMLElement;
    return { fixture, element };
  }

  it('starts at zero', async () => {
    const { fixture, element } = setup();
    await fixture.whenStable();
    expect(element.querySelector('h2')?.textContent).toContain('Counter: 0');
  });

  it('increases when the button is clicked', async () => {
    const { fixture, element } = setup();
    const buttons = element.querySelectorAll('button');

    buttons[0].click(); // Increase
    buttons[0].click();
    await fixture.whenStable();

    expect(element.querySelector('h2')?.textContent).toContain('Counter: 2');
  });

  it('resets to zero', async () => {
    const { fixture, element } = setup();
    const buttons = element.querySelectorAll('button');

    buttons[0].click(); // Increase
    buttons[2].click(); // Reset
    await fixture.whenStable();

    expect(element.querySelector('h2')?.textContent).toContain('Counter: 0');
  });
});

Each test follows the same pattern, often called arrange, act, assert: create the component, do something the user would do, then check the result. fixture.whenStable() waits until Angular has updated the page.

Test the HTTP service

// src/app/posts/post-store.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { PostStore } from './post-store';

describe('PostStore', () => {
  let store: PostStore;
  let http: HttpTestingController;
  const url = 'https://jsonplaceholder.typicode.com/posts';

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    store = TestBed.inject(PostStore);
    http = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    http.verify(); // fails if a request was not expected
  });

  it('loads posts and stops loading', () => {
    store.load();
    expect(store.loading()).toBe(true);

    http.expectOne(url).flush([{ id: 1, userId: 1, title: 'Hello', body: 'World' }]);

    expect(store.posts().length).toBe(1);
    expect(store.loading()).toBe(false);
    expect(store.error()).toBeNull();
  });

  it('shows a friendly message when the server fails twice', () => {
    store.load();

    // retry(1) sends the request a second time, so fail both.
    http.expectOne(url).flush('error', { status: 500, statusText: 'Server Error' });
    http.expectOne(url).flush('error', { status: 500, statusText: 'Server Error' });

    expect(store.loading()).toBe(false);
    expect(store.error()).toContain('Something went wrong');
  });
});

provideHttpClientTesting() replaces the real network. You decide the answer with flush(), so tests are fast and need no internet.

The key parts

  • Test behaviour the user sees (text, buttons), not private details of the class.
  • Keep tests independent. beforeEach gives every test a fresh start.
  • http.verify() in afterEach catches forgotten or unexpected requests.
  • Test the failure paths too, as the second service test does.
  • Name tests as sentences: "increases when the button is clicked".

Try it yourself

  1. Add a test for the Decrease button.
  2. Write a test for the add method of PostStore using http.expectOne(url) with method POST.
  3. Break the counter on purpose and watch the test fail with a helpful message.

Quick check

  1. Which command runs the tests once and exits?
  2. What does provideHttpClientTesting() do?
  3. Why test error cases?

Next, make the app usable for everyone with accessibility. See the testing guide for more.

Frequently asked questions

Do I need to learn Jasmine or Karma?

Only if you maintain an older project. The concepts and the TestBed are the same, and some details differ.

How much should I test?

Start with the logic that would hurt most if it broke: calculations, forms, services and key user flows.