Angular TestBed
Introduction
Angular TestBed is the primary tool for unit testing Angular components and services. It provides a simulated Angular environment where you can create and test components without needing a browser. TestBed acts as a powerful testing utility that allows you to:
- Create components
- Provide dependencies (services)
- Trigger change detection
- Query DOM elements
- Simulate user interactions
For beginners, understanding TestBed is crucial for writing effective tests in Angular applications. This guide will walk you through the fundamentals and practical usage of Angular TestBed.
What is TestBed?
TestBed creates a dynamically-constructed Angular test module that simulates an Angular environment for testing. It allows you to configure the testing module with components, services, pipes, and directives just like you would in a real Angular application.
Think of TestBed as a mini Angular application specifically built for testing one component or service at a time.
Basic TestBed Setup
Let's explore how to set up a simple test using TestBed:
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { HelloComponent } from './hello.component';
describe('HelloComponent', () => {
let component: HelloComponent;
let fixture: ComponentFixture<HelloComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HelloComponent]
}).compileComponents();
fixture = TestBed.createComponent(HelloComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Let's break down the above code:
configureTestingModule()
- Configures a testing module with our componentcompileComponents()
- Compiles the component's template and CSScreateComponent()
- Creates an instance of the componentfixture.componentInstance
- Gets access to the component instancefixture.detectChanges()
- Runs change detection
Component Fixture
The ComponentFixture
is a wrapper around your component that provides testing utilities. It allows you to:
- Access the component instance with
fixture.componentInstance
- Access the native element with
fixture.nativeElement
- Access the debug element with
fixture.debugElement
- Trigger change detection with
fixture.detectChanges()
Here's an example of testing a component's DOM:
it('should display the correct title', () => {
component.title = 'Test Title';
fixture.detectChanges(); // Important: update the view
const titleElement = fixture.nativeElement.querySelector('h1');
expect(titleElement.textContent).toContain('Test Title');
});
Testing Components with Dependencies
Most real components have dependencies like services. TestBed allows you to provide mock versions of these dependencies:
For example, let's test a UserProfileComponent
that depends on a UserService
:
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { UserProfileComponent } from './user-profile.component';
import { UserService } from './user.service';
// Create a mock service
const mockUserService = {
getUser: jasmine.createSpy('getUser').and.returnValue({ name: 'John Doe', email: '[email protected]' })
};
describe('UserProfileComponent', () => {
let component: UserProfileComponent;
let fixture: ComponentFixture<UserProfileComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UserProfileComponent],
providers: [
{ provide: UserService, useValue: mockUserService }
]
}).compileComponents();
fixture = TestBed.createComponent(UserProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should display user name', () => {
expect(fixture.nativeElement.querySelector('.user-name').textContent).toContain('John Doe');
});
it('should call getUser on initialization', () => {
expect(mockUserService.getUser).toHaveBeenCalled();
});
});
Testing Component Interaction
TestBed is great for testing how components interact with the DOM and respond to user events:
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CounterComponent]
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should increment counter when button is clicked', () => {
// Initial value
expect(component.count).toBe(0);
// Find the button and click it
const button = fixture.nativeElement.querySelector('button.increment');
button.click();
// Detect changes
fixture.detectChanges();
// Check the updated value
expect(component.count).toBe(1);
// Check if the display is updated
const countDisplay = fixture.nativeElement.querySelector('.count');
expect(countDisplay.textContent).toContain('1');
});
});
Testing Services with TestBed
TestBed isn't just for components. It's also useful for testing services:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService]
});
service = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Make sure no requests are outstanding
});
it('should retrieve data from API', () => {
const mockData = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];
service.getData().subscribe(data => {
expect(data).toEqual(mockData);
});
const req = httpMock.expectOne('api/data');
expect(req.request.method).toBe('GET');
req.flush(mockData);
});
});
Real-world Example: Testing a Todo List Component
Let's see a more complete, real-world example of testing a todo list component:
First, here's our simple todo component:
// todo-list.component.ts
import { Component } from '@angular/core';
import { TodoService } from './todo.service';
@Component({
selector: 'app-todo-list',
template: `
<div>
<h2>My Todo List</h2>
<input #todoInput placeholder="Add new todo" />
<button (click)="addTodo(todoInput.value); todoInput.value = ''">Add</button>
<ul>
<li *ngFor="let todo of todos; let i = index">
<span [class.completed]="todo.completed">{{ todo.text }}</span>
<button (click)="toggleComplete(i)">Toggle</button>
<button (click)="removeTodo(i)">Remove</button>
</li>
</ul>
<p>{{ todos.length }} total items, {{ completedCount }} completed</p>
</div>
`,
styles: [`.completed { text-decoration: line-through; }`]
})
export class TodoListComponent {
todos: Array<{text: string, completed: boolean}> = [];
constructor(private todoService: TodoService) {}
ngOnInit() {
this.todoService.getTodos().subscribe(todos => {
this.todos = todos;
});
}
addTodo(text: string) {
if (text.trim()) {
this.todoService.addTodo({ text, completed: false })
.subscribe(todo => this.todos.push(todo));
}
}
toggleComplete(index: number) {
const todo = this.todos[index];
todo.completed = !todo.completed;
this.todoService.updateTodo(index, todo).subscribe();
}
removeTodo(index: number) {
this.todoService.deleteTodo(index)
.subscribe(() => this.todos.splice(index, 1));
}
get completedCount() {
return this.todos.filter(todo => todo.completed).length;
}
}
Now, let's write tests for this component:
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
import { TodoListComponent } from './todo-list.component';
import { TodoService } from './todo.service';
import { of } from 'rxjs';
import { By } from '@angular/platform-browser';
describe('TodoListComponent', () => {
let component: TodoListComponent;
let fixture: ComponentFixture<TodoListComponent>;
let mockTodoService: jasmine.SpyObj<TodoService>;
// Sample todo data
const mockTodos = [
{ text: 'Todo 1', completed: false },
{ text: 'Todo 2', completed: true }
];
beforeEach(async () => {
// Create a mock TodoService
mockTodoService = jasmine.createSpyObj('TodoService',
['getTodos', 'addTodo', 'updateTodo', 'deleteTodo']);
// Configure the mock to return data
mockTodoService.getTodos.and.returnValue(of(mockTodos));
mockTodoService.addTodo.and.callFake(todo => of(todo));
mockTodoService.updateTodo.and.callFake((index, todo) => of(todo));
mockTodoService.deleteTodo.and.returnValue(of(undefined));
await TestBed.configureTestingModule({
declarations: [TodoListComponent],
providers: [
{ provide: TodoService, useValue: mockTodoService }
]
}).compileComponents();
fixture = TestBed.createComponent(TodoListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should load todos on init', () => {
expect(mockTodoService.getTodos).toHaveBeenCalled();
expect(component.todos.length).toBe(2);
});
it('should display todos in the template', () => {
const todoItems = fixture.debugElement.queryAll(By.css('li'));
expect(todoItems.length).toBe(2);
expect(todoItems[0].query(By.css('span')).nativeElement.textContent).toContain('Todo 1');
});
it('should add a new todo', fakeAsync(() => {
const newTodo = { text: 'New Todo', completed: false };
mockTodoService.addTodo.and.returnValue(of(newTodo));
// Get the input and button
const input = fixture.debugElement.query(By.css('input')).nativeElement;
const button = fixture.debugElement.query(By.css('button')).nativeElement;
// Set input value and click the button
input.value = 'New Todo';
button.click();
tick(); // Simulate passage of time for async operations
fixture.detectChanges();
// Check if service was called and todo was added
expect(mockTodoService.addTodo).toHaveBeenCalledWith(newTodo);
expect(component.todos.length).toBe(3);
// Check if the new todo appears in the DOM
const todoItems = fixture.debugElement.queryAll(By.css('li'));
expect(todoItems.length).toBe(3);
}));
it('should toggle todo completion status', () => {
// Get the first todo's toggle button and click it
const toggleButton = fixture.debugElement
.queryAll(By.css('li'))[0]
.queryAll(By.css('button'))[0]
.nativeElement;
toggleButton.click();
// Check if the service was called
expect(mockTodoService.updateTodo).toHaveBeenCalledWith(0, { text: 'Todo 1', completed: true });
expect(component.todos[0].completed).toBeTruthy();
});
it('should remove a todo', () => {
// Get the first todo's remove button and click it
const removeButton = fixture.debugElement
.queryAll(By.css('li'))[0]
.queryAll(By.css('button'))[1]
.nativeElement;
removeButton.click();
fixture.detectChanges();
// Check if the service was called and todo was removed
expect(mockTodoService.deleteTodo).toHaveBeenCalledWith(0);
expect(component.todos.length).toBe(1);
// Check if the DOM was updated
const todoItems = fixture.debugElement.queryAll(By.css('li'));
expect(todoItems.length).toBe(1);
});
it('should display the correct count of todos', () => {
const countText = fixture.debugElement.query(By.css('p')).nativeElement.textContent;
expect(countText).toContain('2 total items, 1 completed');
});
});
TestBed Best Practices
- Keep tests focused and isolated: Test one component behavior per test case.
- Use
beforeEach
to reset the test environment: This ensures each test starts with a clean state. - Mock dependencies: Never rely on actual services or API calls in unit tests.
- Always call
detectChanges()
after changing component properties: This ensures the template is updated. - Use
fakeAsync
andtick()
for handling asynchronous operations: This makes your tests more predictable. - Use
debugElement
andBy.css()
for querying elements: This is more reliable than direct DOM queries. - Test both component logic and template bindings: Make sure they work together correctly.
Common TestBed Issues and Solutions
1. "No provider for X" error
// Solution: Provide the dependency in the testing module
TestBed.configureTestingModule({
providers: [
{ provide: DependencyService, useValue: mockDependency }
]
});
2. Component template not rendering
// Solution: Make sure to call detectChanges()
fixture.detectChanges();
3. Async operations not completing in tests
// Solution: Use fakeAsync and tick()
it('should handle async operations', fakeAsync(() => {
component.loadData();
tick(); // Wait for async operations to complete
fixture.detectChanges();
expect(component.data).toBeDefined();
}));
Summary
Angular TestBed is an essential tool for testing components and services in Angular applications. It provides a simulated Angular environment that allows you to:
- Create and test components in isolation
- Mock dependencies and services
- Test DOM interactions and updates
- Verify component behavior
By mastering TestBed, you can ensure your Angular components work correctly and continue to work as your application evolves. Writing good tests with TestBed not only verifies your code works but also serves as documentation for how components should behave.
Additional Resources
Exercises
- Create a simple counter component with increment and decrement buttons, then write TestBed tests to verify it works correctly.
- Create a component that displays user information from a service, and write tests using a mock service.
- Write tests for a form component that validates user input and shows error messages.
- Test a component that uses the Angular router by providing a mock router and activatedRoute.
- Write tests for a component that has parent-child communication using @Input and @Output decorators.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)