Angular unit testing checks isolated pieces of code in an Angular app. It allows users to add new features without interrupting any other part of their application. Jasmine is a JavaScript testing framework and Karma is a node-based testing tool for JavaScript codes across multiple real browsers. This blog helps you get started with Angular unit testing leveraging Karma and Jasmine.
First things first, you must have Angular installed on your machine. That is where you need to start Angular installation. If you already have Angular installed, feel free to skip the next step.
Creating and managing an Angular project is quite easy. There are various competing libraries, frameworks, and tools that can resolve the problems. The Angular team has created Angular CLI, which is a command-line tool used to streamline your Angular projects. Angular CLI is installed via npm, so you are going to need to have Node installed on your machine.
After Node is installed, you can run the following command in your terminal.
The time it takes for installation to complete may change. After it is done, you can see Angular CLI’s version by typing the following command in your terminal.
Now, that you have Angular CLI installed, you are ready to create an Angular sample app. Run the following command in your terminal.
ng new angular-unit-test-application
After executing the command, you will be asked whether you want to add Angular routing. Type Y and press ENTER. You will then be asked to choose between several options of stylesheet formats for your app. It will take a few minutes and once done, you will move to your testing app.
Unit testing tests the isolated units of code. Unit tests aim to answer questions such as,
To answer these questions, it is critical to isolate the unit of code under test. That is because when you are testing the sort function you don't want to be forced into creating related pieces such as making any API calls to fetch the actual database data to sort.
As you already aware that unit testing tests individual components of the software or app. The main motive behind this is to check that all the individual parts are working as intended. A unit is the smallest possible component of software that can be tested. Usually, it has several inputs and one output.
Let’s jump into the Angular web app testing part.
Run the following command in your terminal.
After waiting for a few seconds, you will see a new window of your web browser open on a page looking like this as you see in the below image,
What is Karma test runner?
Karma is a testing automation tool developed by the Angular JS team as it was getting difficult to test their own framework features with current tools. As a result, they developed Karma and transitioned it to Angular as the default test runner for apps developed with the Angular CLI. Apart from getting along with Angular, it offers flexibility to tailor Karma to your workflow. It has an option to test your code across different browsers and devices like tablets, phones, etc. Karma gives you options to substitute Jasmine with other testing frameworks like Mocha and QUnit.
Here is the content of the karma.conf.js file in a sample project.
What is Jasmine?
Jasmine is a free as well as an open-source Behavior Driven Development (BDD) framework that tests JavaScript code and also goes well with Karma. Like Karma, it is also the suggested testing framework within the Angular documentation.
The flow of how the test run looks like,
Testing component add-organization.component.ts
import { TranslateService } from '@ngx-translate/core';
import { SharedService } from 'src/app/shared/services/shared.service';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { appConstants, allowedFileFormat, AppRoutes } from 'src/app/app.constants';
import { SelectOption } from 'src/app/shared/interface/select-option';
import { OrganizationService } from '../organization.service';
import { getOrganizations } from 'src/app/shared/config/api';
import { Router, ActivatedRoute } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from 'src/app/shared/components/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'app-add-organization',
templateUrl: './add-organization.component.html',
styleUrls: ['./add-organization.component.scss']
})
export class AddOrganizationComponent implements OnInit {
orgId: any;
submitted = false;
logoFileFormat = allowedFileFormat.logoFileFormat;
logoFileSize = allowedFileFormat.logoFileSize;
selectedImageLogo!: File;
selectedImageLogoUrl = '';
countryOptions: SelectOption[] = [];
menuList = [{
label: 'HOME',
url: ''
}, {
label: 'ORGANIZATION',
url: '/app/organization'
}, {
label: 'CREATE ORGANIZATION',
url: ''
}];
themeData: any;
configurationTabStatus = true;
loading = false;
userData = [{name: 'name'}];
userCount = 15;
undefinedVariable: any;
currentStep = 1;
completedSteps: number[] = [];
orgForm = this.createForm();
constructor(private fb: FormBuilder, public sharedService: SharedService,
public translateService: TranslateService, private route: Router,
public organizationService: OrganizationService, private activatedRoute: ActivatedRoute,
private confirmDialog: MatDialog) { }
ngOnInit(): void {
this.orgId = this.activatedRoute.snapshot.params['orgId'];
if (this.orgId) {
this.configurationTabStatus = false;
}
this.getCountries();
this.getOrganizationDetails();
}
createForm() {
return this.fb.group({
firstName: ['', [Validators.required, Validators.maxLength(200)]],
lastName: ['', [Validators.required, Validators.maxLength(200)]],
isActive: [true],
email: ['', [Validators.required, Validators.email, Validators.maxLength(200)]],
});
}
get formControl() {
return this.orgForm.controls;
}
isFieldInvalid(field: string) {
return (
(this.formControl[field].invalid && this.formControl[field].touched) ||
(this.formControl[field].untouched && this.submitted && this.formControl[field].invalid)
);
}
displayFieldCss(field: string) {
return {
[appConstants.default.hasErrorClass]: this.isFieldInvalid(field),
};
}
onViewUser() {
if (this.orgId) {
this.sharedService.setOrganizationId(this.orgId);
this.route.navigate([AppRoutes.userPath]);
this.userData = [];
this.submitted = false;
this.userCount = 10;
this.undefinedVariable = undefined;
} else {
this.route.navigate([AppRoutes.addUser]);
this.userData = [{name: 'ZYMR'}];
this.submitted = true;
this.userCount = 20;
this.undefinedVariable = 'Test';
}
}
isCompleted = (step: any) => this.completedSteps.indexOf(step) !== -1;
navigateToStep(step: any) {
if(this.currentStep !== step && (this.orgId || this.isCompleted(step))) {
switch (step) {
case 1:
this.route.navigate([AppRoutes.user + this.orgId]);
break;
case 2:
this.route.navigate([AppRoutes.organization + this.orgId]);
break;
case 3:
this.route.navigate([AppRoutes.userPath + this.orgId]);
break;
case 4:
this.route.navigate([AppRoutes.addUser + this.orgId]);
break;
default:
break;
}
}
}
changeOrgStatus(event: any) {
if (this.orgId && !event.checked) {
const confirmDialog = this.confirmDialog.open(ConfirmDialogComponent, {
disableClose: true,
data: {
title: this.translateService.instant('COMMON.ACTION_CONFIRM.TITLE'),
message: this.translateService.instant('ORGANIZATIONS.ORGANIZATIONS_DEACTIVE'),
},
maxWidth: '100vw',
width: '600px',
});
if (confirmDialog) {
confirmDialog.afterClosed().subscribe(result => {
if (result === true) {
this.formControl['isActive'].setValue(event.checked);
} else {
this.formControl['isActive'].setValue(!event.checked);
}
});
}
}
}
onSubmit(): void {
const formData = new FormData();
formData.append('firstName', this.formControl['firstName'].value);
formData.append('lastName', this.formControl['lastName'].value);
formData.append('isActive', this.formControl['isActive'].value);
formData.append('email', this.formControl['email'].value);
if (this.orgId) {
formData.append('Id', this.orgId);
this.createEditNewOrganization(formData, appConstants.methodType.put);
} else {
this.createEditNewOrganization(formData, appConstants.methodType.post);
}
}
private createEditNewOrganization(formData: FormData, methodType: string): void {
this.submitted = true;
if (this.orgForm.invalid) {
return;
}
this.sharedService.showLoader();
this.organizationService.postFile(getOrganizations, formData, methodType).subscribe({
next: (res: any) => {
this.sharedService.responseHandler(res, true, true, true);
if (this.sharedService.isApiSuccess(res)) {
this.orgId = res.data;
if (methodType === appConstants.methodType.post) {
this.route.navigate([AppRoutes.editOrganization + '/' + this.orgId]);
} else {
this.getOrganizationDetails();
}
}
},
error: (err: any) => {
this.sharedService.errorHandler(err);
},
complete: () => this.sharedService.hideLoader()
}
);
}
private getOrganizationDetails() {
if (this.orgId) {
this.loading = true;
const apiUrl = getOrganizations + '/' + this.orgId;
this.sharedService.showLoader();
this.organizationService.get(apiUrl).subscribe({
next: (res: any) => {
if (this.sharedService.isApiSuccess(res)) {
this.configurationTabStatus = false;
this.selectedImageLogoUrl =
res.data.imageURL ? (res.data.imageURL + '?modifiedDate=' + res.data.modifiedDate) : res.data.imageURL;
const formattedData = this.organizationService.prepareOrganizationDetailsResponse(res.data);
this.orgForm.patchValue(formattedData);
}
},
error: (err: any) => {
this.sharedService.errorHandler(err);
},
complete: () => {
this.loading = false;
this.sharedService.hideLoader();
}
}
);
}
}
private getCountries(): void {
this.sharedService.getCountriesList().subscribe(
(res: Array<SelectOption>) => {
this.countryOptions = res;
}
);
}
}
add-organization.component.spec.ts
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AddOrganizationComponent } from './add-organization.component';
import { HttpClientModule } from '@angular/common/http';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule, TranslateLoader, TranslateFakeLoader } from '@ngx-translate/core';
import { ToastrModule } from 'ngx-toastr';
import { SharedModule } from 'src/app/shared/modules/shared.module';
import { OrganizationService } from '../organization.service';
import { appConstants, AppRoutes } from 'src/app/app.constants';
import { defer } from 'rxjs';
describe('AddOrganizationComponent', () => {
let component: AddOrganizationComponent;
let fixture: ComponentFixture<AddOrganizationComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
RouterTestingModule,
SharedModule,
ToastrModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateFakeLoader
}
})
],
declarations: [AddOrganizationComponent],
providers: [
OrganizationService
]
}).compileComponents();
fixture = TestBed.createComponent(AddOrganizationComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should get organization Form', () => {
component.createForm();
expect(component.formControl).not.toBeNull();
});
it('should page is in edit mode', () => {
(component as any).activatedRoute = {
snapshot: {
params: {
orgId: '123'
}
}
};
spyOn((component as any), 'getCountries');
spyOn((component as any), 'getOrganizationDetails');
component.orgId = '123';
component.ngOnInit();
expect(component.configurationTabStatus).toBeFalsy();
});
it('should initialize country dropdown', waitForAsync(() => {
const countryList = [{
value: 1,
display: 'india',
}];
spyOn((component as any).sharedService, 'getCountriesList').and.returnValue(promiseData(countryList));
(component as any).getCountries();
fixture.whenStable().then(() => {
expect(component.countryOptions).toEqual(countryList);
});
}));
it('should be toggled to deactivated organization', waitForAsync(() => {
component.orgId = '123';
component.createForm();
spyOn((component as any).confirmDialog, 'open').and.returnValue({ afterClosed: () => promiseData(true) });
component.changeOrgStatus({ checked: false });
fixture.whenStable().then(() => {
expect(component.formControl['isActive'].value).toBeFalsy();
});
}));
it('should be toggled activated organization', waitForAsync(() => {
component.orgId = '123';
component.createForm();
spyOn((component as any).confirmDialog, 'open').and.returnValue({ afterClosed: () => promiseData(false) });
component.changeOrgStatus({ checked: false });
fixture.whenStable().then(() => {
expect(component.formControl['isActive'].value).toBeTruthy();
});
}));
it('should save organization details', () => {
component.orgId = '';
const spy = spyOn((component as any), 'createEditNewOrganization');
component.onSubmit();
expect(spy).toHaveBeenCalled();
});
it('should update organization details', () => {
component.orgId = '123';
const spy = spyOn((component as any), 'createEditNewOrganization');
component.onSubmit();
expect(spy).toHaveBeenCalled();
});
it('should save organization data on createEditNewOrganization call', waitForAsync(() => {
component.createForm();
component.orgForm.patchValue({
lastName: 'name',
firstName: 'vatNumber',
email: 'test@gmail.com',
});
spyOn((component as any).organizationService, 'postFile').and.returnValue(promiseData({
code: '',
data: '123',
message: '',
status: appConstants.responseStatus.success
}));
const spy = spyOn((component as any).sharedService, 'showLoader');
const spyResponseHandler = spyOn((component as any).sharedService, 'responseHandler');
const navigateByUrlSpy = spyOn((component as any).route, 'navigateByUrl');
(component as any).createEditNewOrganization({}, appConstants.methodType.post);
fixture.whenStable().then(() => {
expect(spy).toHaveBeenCalled();
expect(spyResponseHandler).toHaveBeenCalled();
expect(navigateByUrlSpy).toHaveBeenCalled();
});
}));
it('should update organization data on createEditNewOrganization call', waitForAsync(() => {
component.createForm();
component.orgForm.patchValue({
lastName: 'name',
firstName: 'vatNumber',
email: 'test@gmail.com',
});
spyOn((component as any).organizationService, 'postFile').and.returnValue(promiseData({
code: '',
data: '123',
message: '',
status: appConstants.responseStatus.success
}));
const spy = spyOn((component as any).sharedService, 'showLoader');
const getOrganizationDetails = spyOn((component as any), 'getOrganizationDetails');
const spyResponseHandler = spyOn((component as any).sharedService, 'responseHandler');
(component as any).createEditNewOrganization({}, appConstants.methodType.put);
fixture.whenStable().then(() => {
expect(spy).toHaveBeenCalled();
expect(getOrganizationDetails).toHaveBeenCalled();
expect(spyResponseHandler).toHaveBeenCalled();
});
}));
it('should org form invalid on createEditNewOrganization call', () => {
component.createForm();
component.orgForm.patchValue({
name: 'name',
});
(component as any).createEditNewOrganization({}, appConstants.methodType.post);
expect(component.submitted).toBeTruthy();
});
it('should handle error while saving organization data on createEditNewOrganization call', waitForAsync(() => {
component.createForm();
component.orgForm.patchValue({
lastName: 'name',
firstName: 'vatNumber',
email: 'test@gmail.com',
});
spyOn((component as any).organizationService, 'postFile').and.returnValue(rejectPromise({
code: '',
data: '',
message: '',
status: appConstants.responseStatus.error
}));
const spyResponseHandler = spyOn((component as any).sharedService, 'errorHandler');
(component as any).createEditNewOrganization({}, appConstants.methodType.post);
fixture.whenStable().then(() => {
expect(spyResponseHandler).toHaveBeenCalled();
});
}));
it('should get organization details on getOrganizationDetails call', waitForAsync(() => {
component.createForm();
component.orgId = '123';
const orgDetails = {
lastName: 'lastName',
firstName: 'firstName',
email: 'test@gmail.com',
isActive: true
};
spyOn((component as any).organizationService, 'get').and.returnValue(promiseData({
code: '',
data: {
lastName: 'lastName',
firstName: 'firstName',
email: 'test@gmail.com',
imageURL: 'http://www.test.com/img1',
modifiedDate: '12-12-12',
isActive: true
},
status: appConstants.responseStatus.success,
message: ''
}));
spyOn(component.sharedService, 'isApiSuccess').and.returnValue(true);
spyOn(component.organizationService, 'prepareOrganizationDetailsResponse').and.returnValue(orgDetails);
(component as any).getOrganizationDetails();
fixture.whenStable().then(() => {
expect(component.selectedImageLogoUrl).toEqual('http://www.test.com/img1?modifiedDate=12-12-12');
expect(component.configurationTabStatus).toBeFalsy();
expect(component.orgForm.value).toEqual({
lastName: 'lastName',
firstName: 'firstName',
email: 'test@gmail.com',
isActive: true,
});
});
}));
it('should get organization details but imageUrl is empty on getOrganizationDetails call', waitForAsync(() => {
component.createForm();
component.orgId = '123';
const orgDetails = {
lastName: 'lastName',
firstName: 'firstName',
email: 'test@gmail.com',
isActive: true
};
spyOn((component as any).organizationService, 'get').and.returnValue(promiseData({
code: '',
data: {
lastName: 'lastName',
firstName: 'firstName',
email: 'test@gmail.com',
imageURL: '',
modifiedDate: '',
isActive: true
},
status: appConstants.responseStatus.success,
message: ''
}));
spyOn(component.sharedService, 'isApiSuccess').and.returnValue(true);
spyOn(component.organizationService, 'prepareOrganizationDetailsResponse').and.returnValue(orgDetails);
(component as any).getOrganizationDetails();
fixture.whenStable().then(() => {
expect(component.selectedImageLogoUrl).toEqual('');
expect(component.configurationTabStatus).toBeFalsy();
expect(component.orgForm.value).toEqual({
lastName: 'lastName',
firstName: 'firstName',
email: 'test@gmail.com',
isActive: true,
});
});
}));
it('should handle error while getting organization details on getOrganizationDetails call', waitForAsync(() => {
component.createForm();
component.orgId = '123';
spyOn((component as any).organizationService, 'get').and.returnValue(rejectPromise({
code: '',
data: {},
status: appConstants.responseStatus.error,
message: ''
}));
const spy = spyOn(component.sharedService, 'errorHandler');
(component as any).getOrganizationDetails();
fixture.whenStable().then(() => {
expect(spy).toHaveBeenCalled();
});
}));
it('should return class on displayFieldCss', () => {
component.createForm();
component.orgForm.controls['email'].setValue('invalid_email@@dotcom');
component.submitted = true;
expect(component.displayFieldCss('email')).toEqual({
'has-error': true
});
});
it('should set organization id and navigate to user list page', () => {
component.orgId = '123';
const spy = spyOn(component.sharedService, 'setOrganizationId');
const navigateSpy = spyOn((component as any).route, 'navigate');
component.onViewUser();
expect(spy).toHaveBeenCalled();
expect(navigateSpy).toHaveBeenCalled();
expect(component.userData.length).toEqual(0);
expect(component.submitted).toBeFalsy();
expect(component.userCount).toBeLessThan(15);
expect(component.undefinedVariable).toBeUndefined();
});
it('should navigate to add user page', () => {
const navigateSpy = spyOn((component as any).route, 'navigate');
component.onViewUser();
expect(navigateSpy).toHaveBeenCalled();
expect(component.userData.length).toEqual(1);
expect(component.userData).toEqual([{name: 'ZYMR'}]);
expect(component.submitted).toBeTruthy();
expect(component.userCount).toBeGreaterThan(15);
expect(component.undefinedVariable).toBeDefined();
});
describe('on step click', () => {
let spyRoute: jasmine.Spy<any>;
beforeEach(waitForAsync(() => {
spyRoute = spyOn((component as any).route, 'navigate');
}));
it('should be navigate to first main info step with event id', () => {
component.completedSteps = [1,2];
component.currentStep = 2;
component.orgId = '10';
component.navigateToStep(1);
expect(spyRoute).toHaveBeenCalledWith([AppRoutes.user + component.orgId]);
});
it('should be navigate to second event detail step with event id', () => {
component.completedSteps = [1,2];
component.currentStep = 1;
component.orgId = '10';
component.navigateToStep(2);
expect(spyRoute).toHaveBeenCalledWith([AppRoutes.organization + component.orgId]);
});
it('should be navigate to third particiant step with event id', () => {
component.completedSteps = [1,2,3];
component.currentStep = 1;
component.orgId = '10';
component.navigateToStep(3);
expect(spyRoute).toHaveBeenCalledWith([AppRoutes.userPath + component.orgId]);
});
it('should be navigate to fourth communication step with event id', () => {
component.completedSteps = [1,2,3,4];
component.currentStep = 3;
component.orgId = '10';
component.navigateToStep(4);
expect(spyRoute).toHaveBeenCalledWith([AppRoutes.addUser + component.orgId]);
});
it('should not navigate to any step', () => {
component.completedSteps = [1,2,3,4,5];
component.currentStep = 3;
component.orgId = null;
component.navigateToStep(5);
expect(spyRoute).not.toHaveBeenCalled();
});
});
});
export const rejectPromise = (msg: any) => defer(() => Promise.reject(msg));
export const promiseData = <T>(data: T) => defer(() => Promise.resolve(data));
1. Angular reactive form
2. Angular reactive form validation
3. Conditional variable and routing calls
4. Multiple cases to cover branches and statement
5. Pop up modal with closing with yes and no
6. Submitting form data and calling the post API and also the error handling part will cover
7. Get organization details using the API calls
So all of these things, you will test in our spec file you can see in the above image it will cover all the statements, functions, branches, and all.
Testing service
1. Organization.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { HttpBaseService } from '../shared/services/httpbase/httpbase.service';
import { LanguageService } from '../shared/services/language/language.service';
@Injectable({
providedIn: 'root'
})
export class OrganizationService extends HttpBaseService {
constructor(httpClient: HttpClient, languageService: LanguageService) {
super(httpClient, languageService);
}
prepareOrganizationListResponse(resList: any[]) {
let organizationList: any = [];
let organization: any = {};
resList.forEach(list => {
organization.lastName = list.lastName,
organization.firstName = list.firstName,
organization.email = list.email,
organization.isActive = list.isActive
organizationList.push(organization);
});
return organizationList;
}
prepareOrganizationDetailsResponse(res: any) {
return {
lastName: res.lastName,
firstName: res.firstName,
email: res.email,
isActive: res.isActive
};
}
}
2. Organization.service.spec.ts
import { SharedModule } from 'src/app/shared/modules/shared.module';
import { HttpClientModule } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { TranslateModule, TranslateLoader, TranslateFakeLoader, TranslateService } from '@ngx-translate/core';
import { HttpBaseService } from '../shared/services/httpbase/httpbase.service';
import { SharedService } from '../shared/services/shared.service';
import { OrganizationService } from './organization.service';
import { OrganizationConfigurationApi, OrganizationListItemUI } from './organization.model';
describe('OrganizationService', () => {
let service: OrganizationService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
SharedModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateFakeLoader
}
}),
],
providers: [
TranslateService,
HttpBaseService,
SharedService,
{ provide: MAT_DIALOG_DATA, useValue: {} },
{ provide: MatDialogRef, useValue: {} }
]
}).compileComponents();
service = TestBed.inject(OrganizationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should be return properly formatted organization list response', () => {
let organization: any = {};
organization.lastName = 'lastName',
organization.firstName = 'firstName',
organization.email = 'test@gmail.com',
organization.isActive = true,
expect(service.prepareOrganizationListResponse(
[
{
lastName: 'lastName',
firstName: 'firstName',
email: 'test@gmail.com',
isActive: true,
}
]
)).toEqual([organization]);
});
it('should be return organization details response', () => {
expect(service.prepareOrganizationDetailsResponse({
lastName: 'lastName',