Skip to main content

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:

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

  1. configureTestingModule() - Configures a testing module with our component
  2. compileComponents() - Compiles the component's template and CSS
  3. createComponent() - Creates an instance of the component
  4. fixture.componentInstance - Gets access to the component instance
  5. fixture.detectChanges() - Runs change detection

Component Fixture

The ComponentFixture is a wrapper around your component that provides testing utilities. It allows you to:

  1. Access the component instance with fixture.componentInstance
  2. Access the native element with fixture.nativeElement
  3. Access the debug element with fixture.debugElement
  4. Trigger change detection with fixture.detectChanges()

Here's an example of testing a component's DOM:

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

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

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

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

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

typescript
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

  1. Keep tests focused and isolated: Test one component behavior per test case.
  2. Use beforeEach to reset the test environment: This ensures each test starts with a clean state.
  3. Mock dependencies: Never rely on actual services or API calls in unit tests.
  4. Always call detectChanges() after changing component properties: This ensures the template is updated.
  5. Use fakeAsync and tick() for handling asynchronous operations: This makes your tests more predictable.
  6. Use debugElement and By.css() for querying elements: This is more reliable than direct DOM queries.
  7. Test both component logic and template bindings: Make sure they work together correctly.

Common TestBed Issues and Solutions

1. "No provider for X" error

typescript
// Solution: Provide the dependency in the testing module
TestBed.configureTestingModule({
providers: [
{ provide: DependencyService, useValue: mockDependency }
]
});

2. Component template not rendering

typescript
// Solution: Make sure to call detectChanges()
fixture.detectChanges();

3. Async operations not completing in tests

typescript
// 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

  1. Create a simple counter component with increment and decrement buttons, then write TestBed tests to verify it works correctly.
  2. Create a component that displays user information from a service, and write tests using a mock service.
  3. Write tests for a form component that validates user input and shows error messages.
  4. Test a component that uses the Angular router by providing a mock router and activatedRoute.
  5. 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! :)