Skip to main content

Angular Component Testing

Introduction

Component testing is a critical part of developing robust Angular applications. Components are the building blocks of Angular applications, responsible for controlling portions of your UI and managing user interactions. By thoroughly testing your components, you ensure they render correctly, respond to user input appropriately, and interact with services and other components as expected.

In this guide, we'll explore how to test Angular components using the Angular Testing Utilities, specifically focusing on:

  • Setting up the testing environment
  • Creating component fixtures
  • Testing component rendering
  • Verifying component behavior
  • Testing component interactions
  • Mocking dependencies

Let's dive in and learn how to properly test your Angular components!

Setting Up Component Tests

Basic Test Configuration

Every component test begins with proper configuration. Angular provides the TestBed utility to create a testing module that simulates an Angular @NgModule.

Here's how to set up a basic component test:

typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ButtonComponent } from './button.component';

describe('ButtonComponent', () => {
let component: ButtonComponent;
let fixture: ComponentFixture<ButtonComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ButtonComponent]
}).compileComponents();

fixture = TestBed.createComponent(ButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create the component', () => {
expect(component).toBeTruthy();
});
});

This test verifies that our component can be successfully created. Let's break down what's happening:

  1. We import the necessary testing utilities and our component
  2. We set up a describe block for our test suite
  3. In beforeEach, we configure the TestBed with our component
  4. We create a fixture and component instance
  5. We call detectChanges() to trigger initial data binding
  6. Our first test simply checks that the component exists

TestBed Configuration with Dependencies

Most components have dependencies such as services, pipes, or child components. You need to include these in your TestBed configuration:

typescript
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
UserProfileComponent,
AvatarComponent, // Child component
HighlightDirective // Directive used in the component
],
imports: [
FormsModule // Required module
],
providers: [
UserService // Service dependency
]
}).compileComponents();
});

Testing Component Rendering

One of the primary aspects to test is whether your component renders correctly.

Testing Element Presence

To check if elements are properly rendered:

typescript
it('should render title', () => {
const titleElement = fixture.nativeElement.querySelector('h1');
expect(titleElement.textContent).toContain('User Profile');
});

Testing Property Binding

To test if properties are correctly bound to the template:

typescript
it('should display the user name', () => {
component.user = { name: 'Alice', email: '[email protected]' };
fixture.detectChanges(); // Important: trigger change detection

const nameElement = fixture.nativeElement.querySelector('.user-name');
expect(nameElement.textContent).toContain('Alice');
});

Testing Dynamic Content

Testing content that changes based on component state:

typescript
it('should show admin controls when user is admin', () => {
component.isAdmin = false;
fixture.detectChanges();
let adminControls = fixture.nativeElement.querySelector('.admin-controls');
expect(adminControls).toBeNull();

component.isAdmin = true;
fixture.detectChanges();
adminControls = fixture.nativeElement.querySelector('.admin-controls');
expect(adminControls).not.toBeNull();
});

Testing Component Behavior

Beyond rendering, we need to verify the component behaves correctly when users interact with it.

Testing Event Handlers

To test method calls when events are triggered:

typescript
it('should call onSave when save button is clicked', () => {
spyOn(component, 'onSave');

const saveButton = fixture.nativeElement.querySelector('#saveBtn');
saveButton.click();

expect(component.onSave).toHaveBeenCalled();
});

Testing Form Interactions

For components with forms:

typescript
it('should update model when form input changes', () => {
const inputElement = fixture.nativeElement.querySelector('input[name="username"]');

inputElement.value = 'newUsername';
inputElement.dispatchEvent(new Event('input'));
fixture.detectChanges();

expect(component.username).toBe('newUsername');
});

Testing Component Interactions

Components often interact with other components through inputs and outputs.

Testing @Input() Properties

typescript
it('should update display when @Input properties change', () => {
component.userData = { name: 'Bob', role: 'Developer' };
fixture.detectChanges();

const roleDisplay = fixture.nativeElement.querySelector('.role');
expect(roleDisplay.textContent).toContain('Developer');
});

Testing @Output() Events

typescript
it('should emit userSelected event when user is clicked', () => {
let selectedUser: any = null;
component.userSelected.subscribe((user: any) => {
selectedUser = user;
});

const testUser = { id: 1, name: 'Charlie' };
component.selectUser(testUser);

expect(selectedUser).toBe(testUser);
});

Testing with Mocked Services

Most components interact with services. We can mock these services for isolated testing.

Creating a Mock Service

typescript
// Create mock service
const mockUserService = jasmine.createSpyObj('UserService',
['getUsers', 'updateUser']);

// Configure the mock's return value
mockUserService.getUsers.and.returnValue(of([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]));

// Provide the mock in TestBed
TestBed.configureTestingModule({
declarations: [UserListComponent],
providers: [
{ provide: UserService, useValue: mockUserService }
]
}).compileComponents();

Testing Interaction with Services

typescript
it('should load users from the service', () => {
component.loadUsers();
fixture.detectChanges();

const userElements = fixture.nativeElement.querySelectorAll('.user-item');
expect(userElements.length).toBe(2);
expect(mockUserService.getUsers).toHaveBeenCalled();
});

Testing Async Operations

Many components perform asynchronous operations. Here's how to test them:

Using fakeAsync

typescript
import { fakeAsync, tick } from '@angular/core/testing';

it('should update users after API call', fakeAsync(() => {
component.loadUsers(); // Triggers async operation

// Simulate passage of time
tick(500);
fixture.detectChanges();

const userElements = fixture.nativeElement.querySelectorAll('.user-item');
expect(userElements.length).toBe(2);
}));

Using async/waitForAsync

typescript
import { waitForAsync } from '@angular/core/testing';

it('should load data asynchronously', waitForAsync(() => {
component.loadData();

fixture.whenStable().then(() => {
fixture.detectChanges();
const dataElement = fixture.nativeElement.querySelector('.data');
expect(dataElement.textContent).toContain('Loaded Data');
});
}));

Real-World Example: Testing a Todo Component

Let's put everything together with a more complex example - testing a Todo component:

First, here's our component:

typescript
// todo.component.ts
@Component({
selector: 'app-todo',
template: `
<div class="todo-container">
<h2>{{ title }}</h2>
<input
#todoInput
type="text"
placeholder="Add new todo"
(keyup.enter)="addTodo(todoInput.value); todoInput.value = ''">

<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.completed">
<input type="checkbox" [checked]="todo.completed" (change)="toggleTodo(todo)">
{{ todo.text }}
<button class="delete-btn" (click)="deleteTodo(todo)">Delete</button>
</li>
</ul>

<div class="summary">
{{ completedCount }} of {{ todos.length }} completed
</div>
</div>
`
})
export class TodoComponent implements OnInit {
@Input() title = 'Todo List';
@Output() todoAdded = new EventEmitter<any>();

todos: Array<{id: number, text: string, completed: boolean}> = [];

constructor(private todoService: TodoService) {}

ngOnInit() {
this.loadTodos();
}

get completedCount() {
return this.todos.filter(todo => todo.completed).length;
}

loadTodos() {
this.todoService.getTodos().subscribe(todos => {
this.todos = todos;
});
}

addTodo(text: string) {
if (!text.trim()) return;

this.todoService.addTodo({ text, completed: false }).subscribe(newTodo => {
this.todos.push(newTodo);
this.todoAdded.emit(newTodo);
});
}

toggleTodo(todo: any) {
todo.completed = !todo.completed;
this.todoService.updateTodo(todo).subscribe();
}

deleteTodo(todo: any) {
this.todoService.deleteTodo(todo.id).subscribe(() => {
this.todos = this.todos.filter(t => t.id !== todo.id);
});
}
}

Now let's test it:

typescript
// todo.component.spec.ts
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { TodoComponent } from './todo.component';
import { TodoService } from './todo.service';
import { of } from 'rxjs';

describe('TodoComponent', () => {
let component: TodoComponent;
let fixture: ComponentFixture<TodoComponent>;
let mockTodoService: any;

// Sample test data
const testTodos = [
{ id: 1, text: 'Learn Angular', completed: false },
{ id: 2, text: 'Write tests', completed: true }
];

beforeEach(async () => {
// Create mock service
mockTodoService = jasmine.createSpyObj('TodoService',
['getTodos', 'addTodo', 'updateTodo', 'deleteTodo']);

// Set up return values
mockTodoService.getTodos.and.returnValue(of(testTodos));
mockTodoService.addTodo.and.returnValue(of({ id: 3, text: 'New Todo', completed: false }));
mockTodoService.updateTodo.and.returnValue(of({}));
mockTodoService.deleteTodo.and.returnValue(of({}));

await TestBed.configureTestingModule({
declarations: [TodoComponent],
providers: [
{ provide: TodoService, useValue: mockTodoService }
]
}).compileComponents();

fixture = TestBed.createComponent(TodoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

// Basic creation test
it('should create', () => {
expect(component).toBeTruthy();
});

// Input property test
it('should display the title', () => {
component.title = 'My Custom Todo List';
fixture.detectChanges();

const titleElement = fixture.nativeElement.querySelector('h2');
expect(titleElement.textContent).toBe('My Custom Todo List');
});

// Rendering & service interaction test
it('should display todos from service', () => {
expect(mockTodoService.getTodos).toHaveBeenCalled();

const todoItems = fixture.nativeElement.querySelectorAll('li');
expect(todoItems.length).toBe(2);
expect(todoItems[0].textContent).toContain('Learn Angular');
expect(todoItems[1].textContent).toContain('Write tests');
});

// Class binding test
it('should mark completed todos with the completed class', () => {
const todoItems = fixture.nativeElement.querySelectorAll('li');
expect(todoItems[1].classList).toContain('completed');
expect(todoItems[0].classList).not.toContain('completed');
});

// User interaction test
it('should add a new todo when enter key is pressed', fakeAsync(() => {
// Spy on component's output event
spyOn(component.todoAdded, 'emit');

// Find input and simulate enter
const inputElement = fixture.nativeElement.querySelector('input[type="text"]');
inputElement.value = 'New Todo';
inputElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));

tick(); // Process async operations
fixture.detectChanges();

// Check if service method was called
expect(mockTodoService.addTodo).toHaveBeenCalledWith({
text: 'New Todo',
completed: false
});

// Check if list was updated
expect(component.todos.length).toBe(3);

// Check if output event was emitted
expect(component.todoAdded.emit).toHaveBeenCalledWith(
{ id: 3, text: 'New Todo', completed: false }
);
}));

// Toggle functionality test
it('should toggle todo completion status', () => {
const todo = component.todos[0];
component.toggleTodo(todo);

expect(todo.completed).toBe(true);
expect(mockTodoService.updateTodo).toHaveBeenCalledWith(todo);
});

// Delete functionality test
it('should delete a todo', fakeAsync(() => {
const todoToDelete = component.todos[0];

// Find and click delete button for first todo
const deleteButton = fixture.nativeElement.querySelectorAll('.delete-btn')[0];
deleteButton.click();

tick(); // Process async operations

expect(mockTodoService.deleteTodo).toHaveBeenCalledWith(todoToDelete.id);
expect(component.todos.length).toBe(1);
expect(component.todos[0].id).toBe(2); // Second todo remains
}));

// Summary calculation test
it('should display correct summary information', () => {
const summaryElement = fixture.nativeElement.querySelector('.summary');
expect(summaryElement.textContent.trim()).toBe('1 of 2 completed');

// Change completion status
component.todos[0].completed = true;
fixture.detectChanges();

expect(summaryElement.textContent.trim()).toBe('2 of 2 completed');
});
});

This comprehensive test suite covers:

  1. Basic component creation
  2. Input property handling
  3. Rendering data from a service
  4. CSS class binding based on state
  5. User interactions (adding todos)
  6. Component methods (toggle, delete)
  7. Computed properties (summary)
  8. Output events

Summary

Testing Angular components thoroughly ensures your application works as expected. Here's what we've covered:

  • Setting up component tests with TestBed
  • Testing component rendering and DOM manipulation
  • Verifying event handling and user interactions
  • Testing input and output bindings
  • Mocking services and dependencies
  • Working with asynchronous operations

By following these practices, you can create reliable, maintainable components and catch issues early in the development process. Remember that good tests not only verify your code works today, but also help ensure changes don't break existing functionality in the future.

Additional Resources

Exercises

  1. Create a LoginComponent with username/password fields and test its form validation
  2. Build a CounterComponent with increment/decrement buttons and test its state changes
  3. Create a DataTableComponent that displays data from a service and test its rendering
  4. Add sorting functionality to the DataTableComponent and test the sorting algorithm
  5. Create a ParentComponent and ChildComponent that communicate via @Input/@Output and test their interaction


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)