Unit Testing in Angular: Best Practices and Tools
Unit testing is an essential part of Angular development as it helps ensure that individual units of code are working correctly. In this tutorial, we will explore the best practices and tools for unit testing in Angular.
Introduction
Unit testing involves testing individual components, services, pipes, directives, and forms in isolation to verify their functionality. By testing each unit separately, developers can catch bugs early on and ensure the overall quality of the application.
Setting up a unit testing environment is the first step in Angular unit testing. We will begin by installing Karma and Jasmine, two popular testing frameworks for Angular.
Installing Karma and Jasmine
To install Karma and Jasmine, run the following commands in your Angular project directory:
npm install karma --save-dev
npm install karma-jasmine jasmine-core --save-dev
Next, create a configuration file for Karma by running:
npx karma init
This will generate a karma.conf.js
file where you can configure Karma for your Angular project.
Writing the first unit test
Now that we have Karma and Jasmine installed, let's write our first unit test. Create a new file app.component.spec.ts
and add the following code:
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});
In this example, we import the TestBed
module from @angular/core/testing
to configure and create a testing module for the AppComponent
. We then use the createComponent
method to create an instance of the AppComponent
and assert that it exists.
Best Practices for Unit Testing
To ensure effective unit testing in Angular, it is important to follow some best practices. Let's explore a few of them.
Isolating components for testing
When testing components, it is important to isolate them from other dependencies and external services. The TestBed
module provides a convenient way to configure and create a testing module for components.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyComponent],
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
In this example, we use the ComponentFixture
to create a fixture for the MyComponent
. We then access the component instance using the componentInstance
property of the fixture.
Mocking dependencies with Jasmine spies
In some cases, components may have dependencies on external services or modules. To isolate the component during testing, we can use Jasmine spies to mock these dependencies.
import { TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
import { MyService } from './my.service';
describe('MyComponent', () => {
let component: MyComponent;
let myService: MyService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyComponent],
providers: [{
provide: MyService,
useValue: jasmine.createSpyObj('MyService', ['getData'])
}]
}).compileComponents();
myService = TestBed.inject(MyService);
component = TestBed.createComponent(MyComponent).componentInstance;
});
it('should call getData method on MyService', () => {
component.getData();
expect(myService.getData).toHaveBeenCalled();
});
});
In this example, we use the provide
property of the TestBed.configureTestingModule
method to provide a mocked version of the MyService
using Jasmine's createSpyObj
function.
Testing asynchronous code
Angular applications often involve asynchronous operations such as HTTP requests or timers. To test asynchronous code, we can use the async
and fakeAsync
functions provided by Angular's testing utilities.
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { MyComponent } from './my.component';
import { MyService } from './my.service';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let myService: MyService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyComponent],
providers: [MyService]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
myService = TestBed.inject(MyService);
});
it('should update data after async operation', fakeAsync(() => {
const testData = 'Test Data';
spyOn(myService, 'getData').and.returnValue(Promise.resolve(testData));
component.getData();
tick();
expect(component.data).toEqual(testData);
}));
});
In this example, we use the fakeAsync
function to wrap our test case and simulate asynchronous behavior. We use tick
to advance the virtual clock and wait for the asynchronous operation to complete.
Using code coverage tools
Code coverage tools help measure how much of your code is being tested. Angular provides built-in support for code coverage using the ng test --code-coverage
command. This generates a coverage report that can be viewed in the browser.
Testing Angular Services
Angular services play a crucial role in application development. Let's explore how to create and test services in Angular.
Creating and testing services
To create a service, run the following command:
ng generate service my-service
This will generate a my-service.service.ts
file that contains the service implementation. To test the service, create a corresponding my-service.service.spec.ts
file and write your tests.
import { TestBed } from '@angular/core/testing';
import { MyService } from './my-service.service';
describe('MyService', () => {
let service: MyService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(MyService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
In this example, we use the TestBed.configureTestingModule
method to configure the testing module for the service. We then use the inject
method to retrieve an instance of the service.
Mocking HTTP requests
Services often make HTTP requests to fetch data from remote servers. To isolate the service during testing, we can mock the HTTP requests using the HttpClientTestingModule
module.
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { MyService } from './my-service.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('MyService', () => {
let service: MyService;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [MyService]
});
service = TestBed.inject(MyService);
httpTestingController = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTestingController.verify();
});
it('should fetch data from the server', fakeAsync(() => {
const testData = 'Test Data';
service.getData().subscribe((data) => {
expect(data).toEqual(testData);
});
const req = httpTestingController.expectOne('http://example.com/api/data');
expect(req.request.method).toEqual('GET');
req.flush(testData);
tick();
}));
});
In this example, we import the HttpClientTestingModule
module and provide it in the testing module. We then use the HttpTestingController
to intercept and mock HTTP requests. We expect a single request to http://example.com/api/data
with the GET method and flush a mock response.
Testing Angular Pipes
Angular pipes are used to transform data in templates. Let's explore how to create and test pipes in Angular.
Creating and testing custom pipes
To create a custom pipe, run the following command:
ng generate pipe my-pipe
This will generate a my-pipe.pipe.ts
file that contains the pipe implementation. To test the pipe, create a corresponding my-pipe.pipe.spec.ts
file and write your tests.
import { MyPipe } from './my-pipe.pipe';
describe('MyPipe', () => {
let pipe: MyPipe;
beforeEach(() => {
pipe = new MyPipe();
});
it('should transform input', () => {
const input = 'Test Input';
const transformed = pipe.transform(input);
expect(transformed).toEqual('Transformed Test Input');
});
});
In this example, we create an instance of the pipe and call the transform
method with an input value. We then assert that the transformed output matches the expected value.
Testing built-in pipes
Angular provides a set of built-in pipes that can be used to format data. To test these pipes, we can leverage the same approach as testing custom pipes.
import { UpperCasePipe } from '@angular/common';
describe('UpperCasePipe', () => {
let pipe: UpperCasePipe;
beforeEach(() => {
pipe = new UpperCasePipe();
});
it('should transform input to uppercase', () => {
const input = 'test input';
const transformed = pipe.transform(input);
expect(transformed).toEqual('TEST INPUT');
});
});
In this example, we import the UpperCasePipe
from @angular/common
and create an instance of the pipe. We then call the transform
method with an input value and assert that the transformed output matches the expected value.
Testing Angular Directives
Angular directives are used to manipulate the DOM. Let's explore how to create and test directives in Angular.
Creating and testing custom directives
To create a custom directive, run the following command:
ng generate directive my-directive
This will generate a my-directive.directive.ts
file that contains the directive implementation. To test the directive, create a corresponding my-directive.directive.spec.ts
file and write your tests.
import { MyDirective } from './my-directive.directive';
describe('MyDirective', () => {
let directive: MyDirective;
beforeEach(() => {
directive = new MyDirective();
});
it('should apply the directive', () => {
// TODO: Write your test case here
});
});
In this example, we create an instance of the directive and write our test case. You can add additional logic to the test case based on your specific directive implementation.
Testing built-in directives
Angular provides a set of built-in directives that can be used to manipulate the DOM. To test these directives, we can leverage the same approach as testing custom directives.
import { NgIf } from '@angular/common';
describe('NgIf', () => {
let directive: NgIf;
beforeEach(() => {
directive = new NgIf(null, null, null);
});
it('should apply the directive', () => {
// TODO: Write your test case here
});
});
In this example, we import the NgIf
directive from @angular/common
and create an instance of the directive. We then write our test case based on the expected behavior of the directive.
Testing Angular Forms
Angular forms play a crucial role in user input handling. Let's explore how to test form validation and submission.
Testing form validation
To test form validation, we can create an instance of the form and set its values. We can then access the form controls and check their validity.
import { TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MyFormComponent } from './my-form.component';
describe('MyFormComponent', () => {
let component: MyFormComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormsModule, ReactiveFormsModule],
declarations: [MyFormComponent]
}).compileComponents();
const fixture = TestBed.createComponent(MyFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should validate form fields', () => {
const form = component.myForm;
const usernameControl = form.controls.username;
usernameControl.setValue('');
expect(usernameControl.valid).toBeFalsy();
usernameControl.setValue('test');
expect(usernameControl.valid).toBeTruthy();
});
});
In this example, we import the FormsModule
and ReactiveFormsModule
to enable form handling in our tests. We create an instance of the MyFormComponent
and access the form controls using the controls
property of the form.
Testing form submission
To test form submission, we can create an instance of the form and simulate user interactions. We can then trigger the form submission and assert the expected outcome.
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MyFormComponent } from './my-form.component';
describe('MyFormComponent', () => {
let component: MyFormComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormsModule, ReactiveFormsModule],
declarations: [MyFormComponent]
}).compileComponents();
const fixture = TestBed.createComponent(MyFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should submit the form', fakeAsync(() => {
spyOn(component, 'onSubmit');
const form = component.myForm;
const submitButton = fixture.nativeElement.querySelector('button[type="submit"]');
form.controls.username.setValue('test');
submitButton.click();
tick();
expect(component.onSubmit).toHaveBeenCalled();
}));
});
In this example, we use Jasmine's spyOn
function to spy on the onSubmit
method of the component. We access the form controls and simulate user interactions by setting the value of the username control and clicking the submit button. We then use tick
to wait for any asynchronous operations to complete before asserting that the onSubmit
method has been called.
Conclusion
In this tutorial, we have explored the best practices and tools for unit testing in Angular. We have learned how to set up a unit testing environment, write unit tests for components, services, pipes, directives, and forms, and follow best practices for effective unit testing. By following these practices and using the provided tools, developers can ensure the quality and reliability of their Angular applications.