Unit testing is an important phase of software development. It helps in adding new enhancements without breaking the existing application features. There are a number of tools and frameworks for writing and running unit test cases. Here in the Angular project, we'll see how to write and run test cases using Jasmine and Karma.
Related Articles
Unit Testing in Angular: how to?
Jasmine is the framework we'll be using for writing unit test cases in Angular. It helps in writing unit tests in a readable format that anyone can understand. We'll be writing unit tests by creating instances of our components and services and by mocking and spying on the methods.
From the official documentation,
Jasmine is a behavior-driven development framework for testing
JavaScript code. It does not depend on any other JavaScript
frameworks. It does not require a DOM. And it has a clean, obvious
syntax so that you can easily write tests.
Karma is more of a tool that helps in running the test case written using the Jasmine framework and displaying the output in the terminal.
From the official documentation,
A simple tool that allows you to execute JavaScript code in multiple real browsers.
How to get started with Unit Testing in Angular
Let's start by creating an Angular project from the very scratch. From your terminal, install the Angular CLI,
npm install -g @angular/cli
Once you have the CLI installed create an Angular project using the following command:
ng new unit-testing-angular
Select Angular routing when asked and select the rest of the configurations.
That will create an Angular project with some boilerplate code. Navigate to the project directory and run the application,
cd unit-testing-angular
npm run start
Now let's add some Angular code so that we can write some unit tests.
Open your app.component.ts
file and add the following code:
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'unit-testing-angular';
public users : User[];
constructor(private http: HttpClient) {
this.users = [];
}
ngOnInit(): void {
//Called after the constructor, initializing input properties, and the first call to ngOnChanges.
//Add 'implements OnInit' to the class.
this.getData();
}
getData(){
this.http.get('https://jsonplaceholder.typicode.com/users').subscribe({
next : (response:any) => {
this.users = response;
},
error : (error) =>{
this.users = [];
}
})
}
}
interface User{
name: string,
email: string
}
The above makes use of the HttpClientModule
so go to your app.module.ts
file and replace it with the following code:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http'
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
For rendering the application you need to add the following HTML code to app.component.html
.
Name
Email
{{user.name}}
{{user.email}}
Save your changes and you will be having a list of names and emails listed on your application page. Now let's write our first Angular unit test case.
Writing Your First Unit Test Case
For writing your first test you don't need to install anything. Jasmine and Karma have already been installed along with some boilerplate code to get started.
Inside your src/app
folder where you have the app.component.html
file, you will also have the app.component.spec.ts
file. That is the unit testing file for your AppComponent
.
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'unit-testing-angular'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('unit-testing-angular');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('unit-testing-angular app is running!');
});
});
Let's try to understand the above code.
The first thing that you'll notice is that you have a couple of imports like TestBed
, RouterTestingModule
in the spec
file. While running a unit test case for a component you first need to configure it using whatever imports
and providers
the component requires. For that you make use of the TestBed
module. As seen from the code, you are making use of TestBed.configureTestingModule
to configure the test bed.
RouterTestingModule
is used for testing the app routing and other related use cases which we can omit for the time being.
You will also notice it
and describe
keywords in the spec file. A unit test case or a spec
is written by using the keyword it
. And describe
keyword is used to group a number of specs.
Our app code makes use of the HttpClientModule
to make API calls. So you need to import it in the spec file.
import { HttpClientTestingModule } from '@angular/common/http/testing';
Add it to the imports inside the testbed configuration. Also, remove the last two test cases and let's just work out the first test case. Here is how the modified spec file looks:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});
Before running the unit test case, let's just look at our first test case. In the above test case, we are expecting the app is getting initialized properly. For that, we are creating an instance of our AppComponent
. If that app instance is created fine it means it's getting initialized fine.
For getting an instance of the component we are making use of the TestBed.createComponent
method where we are passing in the component to create. From the returned response we are getting the component instance which we are checking to be truthful.
Save the above changes and run the unit test case.
npm run test
Once the test case has executed fine you will get the following response in your terminal.
Chrome 108.0.0.0 (Windows 10): Executed 1 of 1 SUCCESS (0.072 secs / 0.054 secs)
TOTAL: 1 SUCCESS
So you succeeded in writing your first unit test or to be precise understanding a simple pre-written test case. Now let's move forward and write some unit tests for the existing Angular component.
Testing Angular component
Let's first write some code for us to unit test. I have added some code to get data from an API endpoint and process the data to show the Name
, Email
and City
in the UI. Add the following code to app.component.ts
file:
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
public users : User[];
constructor(private http: HttpClient) {
this.users = [];
}
ngOnInit(): void {
//Called after the constructor, initializing input properties, and the first call to ngOnChanges.
//Add 'implements OnInit' to the class.
this.getData();
}
getData(){
this.http.get('https://jsonplaceholder.typicode.com/users').subscribe({
next : (response:any) => {
if(response && response.length){
this.users = response.map((u:any) => {
u['city'] = this.getCity(u['address']);
return u;
});
} else {
this.users = [];
}
},
error : (error) =>{
this.users = [];
}
})
}
getCity(address:any){
if(address.city){
return `Residing at ${address.city}`
}
return "No city specified";
}
}
interface User{
name: string,
email: string,
city: string
}
In the ngOnInit
we are calling the getData
method which makes the API call and after parsing the city using the getCity
method we are filling the users
array.
As you noticed, we have two methods in our components getData
and getCity
. Let's write a unit test for testing the method getCity
.
Now, this method getCity
has two scenarios that need to be covered. One when the address has a city and one when it does have a city.
We'll create a component and using its instance we'll invoke the getCity
method and validate its response.
it(`should return the city'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.getCity({city : "Delhi"})).toEqual(`Residing at Delhi`);
});
it(`should return the city not found'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.getCity({})).toEqual(`No city specified`);
});
We wrote test cases, one for an address with a city and one without any city. In both the test cases, we are comparing the response if appropriate to validate the test case. Save the above changes and run the test case.
npm run test
You will be able to see a message that the test cases ran successfully.
Chrome 108.0.0.0 (Windows 10): Executed 3 of 3 SUCCESS (0.126 secs / 0.055 secs)
TOTAL: 3 SUCCESS
Now let's move forward and write another test case to cover the getData
method.
getData
method uses observables or subscriptions while making API calls. Let's learn how to write unit tests for testing methods with a subscription.
Testing Subscriptions
In the existing code, it's a bit difficult to mock the API call since the subscription code and the logic is coupled. So while writing code make sure to divide your code into small logical units which makes it easier to unit test.
I'll break down the existing code getData
into two separate methods. For that create an Angular service using the following code:
ng g service data
The above command will create a .spec
file for DataService
. You can delete it since we'll be focusing only on the app.component.spec
file.
Add the following code to data.service.ts
file:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient) { }
fetchUsersData(){
return this.http.get('https://jsonplaceholder.typicode.com/users')
}
}
Here is the modified app.component.ts
file:
import { Component } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
public users : User[];
constructor(private dataService: DataService) {
this.users = [];
}
ngOnInit(): void {
//Called after the constructor, initializing input properties, and the first call to ngOnChanges.
//Add 'implements OnInit' to the class.
this.getData();
}
getData(){
this.dataService.fetchUsersData().subscribe({
next : (response:any) => {
if(response && response.length){
this.users = response.map((u:any) => {
u['city'] = this.getCity(u['address']);
return u;
});
} else {
this.users = [];
}
},
error : (error) =>{
this.users = [];
}
})
}
getCity(address:any){
if(address.city){
return `Residing at ${address.city}`
}
return "No city specified";
}
}
interface User{
name: string,
email: string,
city: string
}
Let's write a test case to unit test getData
. getData
makes a call to fetchUsersData
which returns an observable. So instead of making an actual API call via fetchUsersData
we'll mock the call to fetchUsersData
.
Let's start by defining the test case and creating a reference to the AppComponent
.
it(`should return empty list of users'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
});
For mocking you can make use of spyOn
. Start by importing the DataService
.
import { DataService } from './data.service';
For using DataService
you first need to create a reference to the service DataService
it(`should return empty list of users'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
let service = fixture.debugElement.injector.get(DataService);
});
Using the service reference you can set up a mock call using the callFake
method as shown in the below code.
it(`should return empty list of users'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
let service = fixture.debugElement.injector.get(DataService);
spyOn(service,"fetchUsersData").and.callFake(() => {
return of([]);
});
app.getData();
expect(app.users).toEqual([]);
});
In the above code we are mocking the fetchUsersData
method to return an empty list. Once the mock has been set we are calling the getData
method and validate the output.
Save the above changes and start the unit tests.
npm run test
Similarly, let us add one more test case with some data instead of an empty array.
it(`should return list of users'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
let service = fixture.debugElement.injector.get(DataService);
spyOn(service,"fetchUsersData").and.callFake(() => {
return of([{city:"Delhi",address:"New Delhi"}]);
});
app.getData();
expect(app.users.length).toEqual(1);
});
In the above test case, I'm passing in some data and expecting the user length to be one. That's how you can unit test observables.
Wrapping It up
In this tutorial, you learned how to get started with writing unit test cases for your Angular application. You learned how to make use of spyOn
and callFake
while testing subscriptions and observables. You can further explore different aspects of Angular unit testing by visiting the official documentation.
The source code from this tutorial is available on GitHub.