Unit testing is a crucial part of software development that ensures individual units of code (such as functions, methods, or classes) work as expected. In Angular, unit testing is typically done using Jasmine and Karma.
Key Concepts
-
Unit Testing Frameworks:
- Jasmine: A behavior-driven development framework for testing JavaScript code.
- Karma: A test runner that allows you to execute your tests in various browsers.
-
Test Bed:
- Angular's
TestBed
is a utility that provides a way to configure and initialize an environment for unit testing Angular components and services.
- Angular's
-
Mocking:
- Creating mock objects to simulate the behavior of real objects in a controlled way.
Setting Up Unit Testing
Angular projects created with the Angular CLI come with Jasmine and Karma pre-configured. To run tests, use the following command:
This command will start the Karma test runner and execute all the tests in your project.
Writing Your First Unit Test
Let's start with a simple example of a unit test for a service.
Example: Testing a Service
Consider a simple CalculatorService
:
// calculator.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class CalculatorService { add(a: number, b: number): number { return a + b; } subtract(a: number, b: number): number { return a - b; } }
Writing Tests for CalculatorService
Create a test file calculator.service.spec.ts
:
// calculator.service.spec.ts import { TestBed } from '@angular/core/testing'; import { CalculatorService } from './calculator.service'; describe('CalculatorService', () => { let service: CalculatorService; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(CalculatorService); }); it('should be created', () => { expect(service).toBeTruthy(); }); it('should add two numbers', () => { expect(service.add(2, 3)).toEqual(5); }); it('should subtract two numbers', () => { expect(service.subtract(5, 3)).toEqual(2); }); });
Explanation
describe
: A Jasmine function that groups related tests.beforeEach
: A function that runs before each test to set up the testing environment.it
: A function that defines an individual test case.expect
: A function that defines an assertion.
Testing Components
Testing components involves more complexity due to the need to handle templates, styles, and component interactions.
Example: Testing a Component
Consider a simple CounterComponent
:
// counter.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-counter', template: ` <button (click)="increment()">Increment</button> <p>{{ count }}</p> ` }) export class CounterComponent { count = 0; increment() { this.count++; } }
Writing Tests for CounterComponent
Create a test file counter.component.spec.ts
:
// counter.component.spec.ts import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CounterComponent } from './counter.component'; import { By } from '@angular/platform-browser'; describe('CounterComponent', () => { let component: CounterComponent; let fixture: ComponentFixture<CounterComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ CounterComponent ] }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(CounterComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should increment count on button click', () => { const button = fixture.debugElement.query(By.css('button')).nativeElement; button.click(); fixture.detectChanges(); const countElement = fixture.debugElement.query(By.css('p')).nativeElement; expect(countElement.textContent).toBe('1'); }); });
Explanation
ComponentFixture
: A wrapper around a component and its template.By
: A utility to query elements in the component's template.fixture.detectChanges()
: Triggers change detection to update the template.
Practical Exercises
Exercise 1: Testing a Service
Create a MathService
with methods for multiplication and division. Write unit tests to verify the correctness of these methods.
Solution
// math.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class MathService { multiply(a: number, b: number): number { return a * b; } divide(a: number, b: number): number { if (b === 0) { throw new Error('Division by zero'); } return a / b; } }
// math.service.spec.ts import { TestBed } from '@angular/core/testing'; import { MathService } from './math.service'; describe('MathService', () => { let service: MathService; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(MathService); }); it('should be created', () => { expect(service).toBeTruthy(); }); it('should multiply two numbers', () => { expect(service.multiply(2, 3)).toEqual(6); }); it('should divide two numbers', () => { expect(service.divide(6, 3)).toEqual(2); }); it('should throw error on division by zero', () => { expect(() => service.divide(6, 0)).toThrow(new Error('Division by zero')); }); });
Exercise 2: Testing a Component
Create a GreetingComponent
that displays a greeting message. Write unit tests to verify the message is displayed correctly.
Solution
// greeting.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-greeting', template: `<p>{{ message }}</p>` }) export class GreetingComponent { message = 'Hello, World!'; }
// greeting.component.spec.ts import { ComponentFixture, TestBed } from '@angular/core/testing'; import { GreetingComponent } from './greeting.component'; describe('GreetingComponent', () => { let component: GreetingComponent; let fixture: ComponentFixture<GreetingComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ GreetingComponent ] }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(GreetingComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should display greeting message', () => { const messageElement = fixture.debugElement.query(By.css('p')).nativeElement; expect(messageElement.textContent).toBe('Hello, World!'); }); });
Common Mistakes and Tips
- Not using
fixture.detectChanges()
: Always callfixture.detectChanges()
after making changes to the component to ensure the template is updated. - Mocking dependencies: Use Angular's
TestBed
to provide mock services or dependencies to isolate the unit being tested. - Testing implementation details: Focus on testing the behavior and output rather than the internal implementation details.
Conclusion
Unit testing is an essential practice in Angular development that helps ensure the reliability and correctness of your code. By using Jasmine and Karma, you can write and run tests for your services, components, and other units of code. Remember to follow best practices and avoid common pitfalls to make your tests effective and maintainable.
In the next section, we will delve into component testing, where we will explore more advanced scenarios and techniques for testing Angular components.
Angular 2+ Course
Module 1: Introduction to Angular
Module 2: TypeScript Basics
- Introduction to TypeScript
- TypeScript Variables and Data Types
- Functions and Arrow Functions
- Classes and Interfaces