Before jump on to this make sure you have cleared your basics: Jasmin Testing Framework: A Comprehensive Guide
Unit testing is a crucial aspect of Angular development, ensuring the correctness and reliability of components, services, pipes, and other parts of the application. This guide will walk you through how to set up tests for Angular components and services, along with various techniques like mocking, isolated tests, and using TestBed for dependency injection.
Angular uses Jasmin and karma by default for unit testing.
1. Service Testing
Angular services contain business logic, and testing them ensures that your core logic functions as expected. Here, we'll go through a basic service example and its tests.
Example: CalculatorService
export class CalculatorService {
add(n1: number, n2: number): number {
return n1 + n2;
}
subtract(n1: number, n2: number): number {
return n1 - n2;
}
}
Test Case:
describe('Calculator Service', () => {
let calculator: CalculatorService;
beforeEach(() => {
calculator = new CalculatorService();
});
it('should add two numbers', () => {
let result = calculator.add(2, 3);
expect(result).toBe(5);
});
it('should subtract two numbers', () => {
let result = calculator.subtract(5, 3);
expect(result).toBe(2);
});
});
2. Testing Services with Dependency Injection
Services can depend on other services via dependency injection. Let's explore a scenario where CalculatorService
depends on a LoggerService
to log actions.
Example: LoggerService
export class LoggerService {
msg: string[] = [];
log(message: string): void {
this.msg.push(message);
}
}
Updated CalculatorService with LoggerService:
export class CalculatorService {
constructor(private loggerService: LoggerService) {}
add(n1: number, n2: number): number {
let result = n1 + n2;
this.loggerService.log('Addition performed');
return result;
}
subtract(n1: number, n2: number): number {
let result = n1 - n2;
this.loggerService.log('Subtraction performed');
return result;
}
}
Test Case with Mocking the LoggerService:
describe('Calculator Service with Logger', () => {
let calculator: CalculatorService;
let mockLoggerService: jasmine.SpyObj<LoggerService>;
beforeEach(() => {
mockLoggerService = jasmine.createSpyObj('LoggerService', ['log']);
calculator = new CalculatorService(mockLoggerService);
});
it('should add two numbers and log the operation', () => {
let result = calculator.add(2, 3);
expect(result).toBe(5);
expect(mockLoggerService.log).toHaveBeenCalledWith('Addition performed');
});
it('should subtract two numbers and log the operation', () => {
let result = calculator.subtract(5, 3);
expect(result).toBe(2);
expect(mockLoggerService.log).toHaveBeenCalledWith('Subtraction performed');
});
});
3. Types of Mocking
Mocking helps isolate tests and avoid calling real services. There are three types of mocking in unit tests:
- Stub: Provides hardcoded responses to method calls.
- Dummy: Used to fill method parameters without returning meaningful data.
- Spy: Records the calls made to a method, useful for verifying interactions.
Using Jasmine’s createSpyObj
for Spies:
let mockLoggerService = jasmine.createSpyObj('LoggerService', ['log']);
This spy can track how many times the log
method is called and with which arguments.
4. Isolated Test Case
An isolated test case tests a single unit in isolation without its dependencies. For example, testing CalculatorService
without involving LoggerService
:
describe('CalculatorService (Isolated Test)', () => {
let calculator: CalculatorService;
beforeEach(() => {
calculator = new CalculatorService();
});
it('should add numbers correctly', () => {
expect(calculator.add(2, 3)).toBe(5);
});
it('should subtract numbers correctly', () => {
expect(calculator.subtract(5, 3)).toBe(2);
});
});
5. Using beforeEach
for Setup
To avoid repetitive setup in each test, we can use beforeEach
to initialize components or services before each test case.
describe('CalculatorService', () => {
let calculator: CalculatorService;
beforeEach(() => {
calculator = new CalculatorService();
});
it('should add two numbers', () => {
expect(calculator.add(2, 3)).toBe(5);
});
it('should subtract two numbers', () => {
expect(calculator.subtract(5, 3)).toBe(2);
});
});
6. Testing Pipes
Pipes in Angular transform data. Let's test a custom pipe that converts numbers into strength descriptions.
Example Pipe:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({name: 'strength'})
export class StrengthPipe implements PipeTransform {
transform(value: number): string {
if (value < 10) {
return `${value} (weak)`;
} else if (value < 20) {
return `${value} (strong)`;
} else {
return `${value} (strongest)`;
}
}
}
Test Case for Pipe:
describe('StrengthPipe', () => {
let pipe: StrengthPipe;
beforeEach(() => {
pipe = new StrengthPipe();
});
it('should transform number to "weak" for values less than 10', () => {
expect(pipe.transform(5)).toBe('5 (weak)');
});
it('should transform number to "strong" for values between 10 and 20', () => {
expect(pipe.transform(15)).toBe('15 (strong)');
});
it('should transform number to "strongest" for values greater than 20', () => {
expect(pipe.transform(25)).toBe('25 (strongest)');
});
});
7. Component Testing with Input and Output
When testing components that interact with user input or emit events, we need to handle the @Input
and @Output
properties.
Component with @Input
and @Output
:
@Component({
selector: 'app-post',
template: `
<div>
<p>{{ post.title }}</p>
<button (click)="onDelete()">Delete</button>
</div>
`
})
export class PostComponent {
@Input() post!: Post;
@Output() delete = new EventEmitter<Post>();
onDelete() {
this.delete.emit(this.post);
}
}
Test Case for Component's Output:
describe('PostComponent', () => {
let component: PostComponent;
let fixture: ComponentFixture<PostComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [PostComponent],
});
fixture = TestBed.createComponent(PostComponent);
component = fixture.componentInstance;
});
it('should raise an event when delete is clicked', () => {
const post: Post = { id: 1, title: 'Post 1', body: 'Body 1' };
component.post = post;
spyOn(component.delete, 'emit');
component.onDelete();
expect(component.delete.emit).toHaveBeenCalledWith(post);
});
});
8. Using TestBed to Test Components
TestBed
helps to set up the testing environment by creating the Angular testing module and resolving dependencies.
Component Setup Using TestBed:
describe('PostComponent with TestBed', () => {
let fixture: ComponentFixture<PostComponent>;
let component: PostComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [PostComponent],
});
fixture = TestBed.createComponent(PostComponent);
component = fixture.componentInstance;
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
});
Accessing Template Elements:
To test template bindings or DOM manipulations, we use nativeElement
or debugElement
.
it('should display the post title', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
const p = bannerElement.querySelector('p')!;
expect(p.textContent).toEqual('Post 1');
});
Summary
Introduction to Jasmine & Karma: Jasmine is a behavior-driven development (BDD) framework for testing JavaScript, while Karma is a test runner that executes tests in real browsers. They work together to simplify testing in Angular applications.
Testing Services: Unit testing services is explained with an emphasis on using Jasmine’s
SpyObj
to mock service dependencies, ensuring isolation in tests.Component Testing: Testing components involves simulating input and output interactions and using
TestBed
to create a testing module.Mocking & Spying: Mocking methods with spies allows you to track function calls and control method behavior in isolated tests.
Unit testing in Angular can seem overwhelming at first, but with practice, you will learn how to write tests that are clear, isolated, and maintainable. By using techniques like mocking, beforeEach
, and TestBed
, you can create robust tests that ensure your Angular applications behave as expected.
Top comments (0)