Angular unit testing using Karma and Jasmine

Jasmine is a JavaScript testing framework, while Karma is a node-based testing tool for JavaScript code across multiple real browsers.

Angular unit tests examine isolated pieces of code in an Angular application. It allows users to add new functionality without disrupting any other part of their application. Jasmine is a JavaScript testing framework, while Karma is a node-based testing tool for JavaScript code across multiple real browsers . This blog helps you get started with Angular unit testing using Karma and Jasmine.

Introduction to Angular Unit Testing

First, Angular must be installed on your machine. This is where you need to start installing Angular. If you already have Angular installed, skip the next step.

Creating and managing Angular projects is very simple. There are various competing libraries, frameworks, and tools that solve these problems. The Angular team created Angular CLI, a command line tool for simplifying Angular projects. Angular CLI is installed via npm, so you need to have Node installed on your machine.

After installing Node, you can run the following commands in the terminal.

The time required to complete the installation may vary. Once completed, you can check the version of Angular CLI by typing the following command in the terminal.

Now that you have the Angular CLI installed, you are ready to create the Angular sample application. Run the following command in your terminal.

ng new angular unit testing application

After executing the command, you will be asked if you want to add Angular routes. Type Y and press ENTER. You will then be asked to choose between several options for stylesheet formatting for your application. This will take a few minutes and once completed; you will be directed to your test application.

Unit tests test isolated units of code. Unit tests are designed to answer the following questions,

  • Does the sort function sort the list in the correct order?
  • Am I able to think logically correctly?

To answer these questions, it is crucial to isolate the unit of code under test. This is because when you are testing the sorting functionality, you don't want to be forced to create the relevant parts, such as making any API calls to get the actual database data to be sorted.

You already know that unit tests test individual components of a software or application. The main motivation behind this is to check that all the individual parts are working as expected. A unit is the smallest software component that can be tested. Typically, it has multiple inputs and one output.

Let's get into the Angular web application 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 the page as shown below.

Interpret the role of Karma and Jasmine in Angular unit testing

What is the Karma test runner?

Karma is a test automation tool developed by the Angular JS team because testing their own framework functionality was becoming increasingly difficult with current tools. Therefore, they developed Karma and converted it to Angular as the default test runner for applications developed using the Angular CLI. In addition to getting along with Angular, it also provides the flexibility to customize Karma according to your workflow. It has the option to test your code on different browsers and devices like tablets, mobile phones, etc. Karma gives you the option to replace Jasmine with other testing frameworks such as Mocha and QUnit .

This is the content of the karma.conf.js file in the sample project.

What is jasmine? Jasmine is a free and open source Behavior Driven Development (BDD) framework that can test JavaScript code and works well with Karma. Like Karma, it is also the recommended testing framework in the Angular documentation.

test run process,

test componentadd-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: '[email protected]',
});
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: '[email protected]',
});
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: '[email protected]',
});
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: '[email protected]',
isActive: true
};
spyOn((component as any).organizationService, 'get').and.returnValue(promiseData({
code: '',
data: {
lastName: 'lastName',
firstName: 'firstName',
email: '[email protected]',
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: '[email protected]',
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: '[email protected]',
isActive: true
};
spyOn((component as any).organizationService, 'get').and.returnValue(promiseData({
code: '',
data: {
lastName: 'lastName',
firstName: 'firstName',
email: '[email protected]',
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: '[email protected]',
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));

So in this component you can see that you have a list of tests that lists the following:

2. Angular responsive form validation

3. Condition variables and routing calls

4. Multiple cases cover branches and statements

5. Pop up modal to close with yes and no

6. Submitting form data and calling post API and error handling will be covered

7. Get organization details using API calls

So all of these things, you're going to be testing in our spec files. You can see in the image above that it will cover all statements, functions, branches, etc.

From the above code snippet, you will notice a lot of things. What each of these does is explained below: 

  1. describeWe start our test with a , where we give the name of the component we are testing within.
  2. We can execute some code before executing each specification. This feature is beneficial for running common code in applications.
  3. Inside beforeEach, we have TestBed.ConfigureTestingModule. TestBedSetting up the configuration and initializing the module suitable for our environment test.ConfigureTestingModulesettings allows us to test the component. You could say it creates a module for our test environment and contains declarations, imports and providers within it.

Testing services

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 = '[email protected]',
organization.isActive = true,

expect(service.prepareOrganizationListResponse(
[
{
lastName: 'lastName',
firstName: 'firstName',
email: '[email protected]',
isActive: true,
}
]
)).toEqual([organization]);
});

it('should be return organization details response', () => {
expect(service.prepareOrganizationDetailsResponse({
lastName: 'lastName',

Guess you like

Origin blog.csdn.net/qq_29639425/article/details/131207250