Testing basics
A test is code that checks other code: it creates a service or component, performs an action and compares the result with the expected one. Tests run in seconds and show at once when a change breaks something. For every file created with ng generate, the CLI has already made a test skeleton ending in .spec.ts.
Running tests
ng testThe command builds the tests and runs them, and again after every save. Angular 20 uses Karma and Jasmine by default, with tests running in a browser. In Angular 21 and newer, new projects use Vitest. The describe, it and expect functions look the same in simple tests.
Testing a service
Open src/app/todo-store.spec.ts and describe the behavior of the task store:
// src/app/todo-store.spec.ts
import { TestBed } from '@angular/core/testing';
import { TodoStore } from './todo-store';
describe('TodoStore', () => {
let store: TodoStore;
beforeEach(() => {
store = TestBed.inject(TodoStore);
});
it('adds a task', () => {
store.add('Buy bread');
expect(store.todos().length).toBe(1);
expect(store.todos()[0].title).toBe('Buy bread');
});
it('counts open tasks', () => {
store.add('First');
store.add('Second');
store.toggle(store.todos()[0].id);
expect(store.left()).toBe(1);
});
});describe groups tests, it is a single test, and expect(...).toBe(...) is a check. TestBed is Angular's test environment; its inject() method returns a service just like inject() in a component. A new environment is created for every test, so the tasks from the first test do not leak into the second.
The terminal shows a report: passed tests are marked green, failed ones red, with the expected and actual values.
Testing a component
A component is created with TestBed.createComponent(), and the resulting HTML is checked:
// src/app/todo-item/todo-item.spec.ts
import { TestBed } from '@angular/core/testing';
import { TodoItem } from './todo-item';
describe('TodoItem', () => {
it('shows the task title', () => {
TestBed.configureTestingModule({ imports: [TodoItem] });
const fixture = TestBed.createComponent(TodoItem);
fixture.componentRef.setInput('todo', { id: 1, title: 'Call Oleg', done: false });
fixture.detectChanges();
const el: HTMLElement = fixture.nativeElement;
expect(el.textContent).toContain('Call Oleg');
});
});setInput() sets an input the way a parent would. detectChanges() asks Angular to update the template, and nativeElement is the component's root element, where you can look for text and tags.
What to test
| What | Example check |
|---|---|
| Service logic | After add() the list has one more task |
| Calculations | left() counts only open tasks |
| Component output | The task title is in the HTML |
| Events | After a click on the checkbox the toggle output fired |
Start with services: they hold the most logic and are the easiest to test.
Common mistake
Forgetting fixture.detectChanges() after setInput() or a data change. The template stays in its old state, and the text check fails even though the component works correctly. After every data change in a component test, call detectChanges().