Web Development

Unit Testing Angular App

October 17th, 2023 | By Jay Raj | 16 min read

In this tutorial, we'll learn how to unit test a simple functional form in an Angular app. We'll be creating the form using Reactive forms, we'll be adding a couple of fields, and validating and submitting the form. We'll go over how to unit-test the form-related features and code.


In one of our previous tutorials on Angular unit testing, we learned how to start with Angular unit testing. I would recommend reading it before you start with this tutorial because it would give you a basic understanding of Angular unit testing.


Getting Started with Unit Testing Angular App


I'll walk you through the code so that you don't feel alienated when we start with unit testing the code. Our approach will be to understand what the code does and then write a unit test for it.


We are using Angular Material UI component for this project. Each and every component that is being used for this project is imported into the `app.module.ts` file.


Creating Angular app


Start by creating an Angular project from scratch using the Angular CLI which you can install using `npm`.

bash
npm install -g @angular/cli


After the Angular CLI is installed create your Angular boilerplate project using

bash
ng new form-app


We are using Angular Material UI for the project. You can add Material UI components to the project using the following command:

bash
ng add @angular/material


By default, you have `AppComponent` inside the project. You can remove the existing code from the `app.component.html` file leaving just the route outlet. Here is how the `app.component.ts` file looks:

html
<router-outlet></router-outlet>


Now create a new component called `HomeComponent`.

ng g component home


This will create a new folder `src/app/home` with the required component files inside it.


Modify the routing file to display the Home Component when the app loads. Here is how the `app-routing.module.ts` file looks:

typescript
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';

const routes: Routes = [{
  path: "", component: HomeComponent
}];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }


Now if you run your application it should load the Home component by default.


Creating Angular Reactive Forms


The form that we will be unit testing is being implemented in the `HomeComponent` inside `src/app/home` folder. The form design is created using Angular material components and can be found inside the `home.component.html` file.


Here is what the `home.component.html` file looks like:

html
<mat-card style="margin: 100px;background-color: beige;">
  <mat-card-content>
    <form style="    display: flex;
        flex-direction: column;
        align-items: center;" [formGroup]="profileForm" (ngSubmit)="onSubmit()">
      <mat-form-field appearance="fill">
        <mat-label>Enter name</mat-label>

        <input id="firstName" matInput formControlName="firstName" placeholder="enter your name">
        <mat-error *ngIf="f && f['firstName'] && f['firstName'].errors?.['required']">
          Name is required.
        </mat-error>
      </mat-form-field>

      <mat-form-field appearance="outline">
        <mat-label>Email address</mat-label>
        <input matInput formControlName="emailAddress" placeholder="enter your email address">
        <mat-error *ngIf="f && f['emailAddress'] && f['emailAddress'].errors?.['required']">
          Email address is required.
        </mat-error>
      </mat-form-field>

      <mat-form-field appearance="outline">
        <mat-label>Phone Number</mat-label>
        <input matInput formControlName="phoneNumber" placeholder="enter your phone number">
        <mat-error *ngIf="f && f['phoneNumber'] && f['phoneNumber'].errors?.['required']">
          Phone number is required.
        </mat-error>
      </mat-form-field>
      <button mat-raised-button color="primary">Save</button>

    </form>
  </mat-card-content>
</mat-card>


Now, on loading the page we have a feature where we’re showing some default data when the form loads. For that, we have already defined a default data object in our `home.component.ts` file and we are setting the data inside the `ngOnInit` hook.


Here is how the `home.component.ts` file looks:

typescript
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css'],
})
export class HomeComponent {

  defaultData : any = {
    firstName : "Roy",
    emailAddress : "[email protected]",
    phoneNumber : "+919895590754"
  }
  profileForm :FormGroup= new FormGroup({});

  get f() { return this.profileForm.controls; }

  constructor(private _formBuilder: FormBuilder){}
  ngOnInit(): void {
    this.profileForm =  this._formBuilder.group({
      firstName: [''],
      emailAddress: [''],
      phoneNumber: ['']
    })
    this.profileForm.setValue({firstName : this.defaultData.firstName ,emailAddress :
this.defaultData.emailAddress, phoneNumber : this.defaultData.phoneNumber})
  }
  onSubmit() {
    try{
    } catch(error){
    }
  }
}


For the above material UI component to work you need to import the component inside the `app.module.ts` file.

typescript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    MatFormFieldModule,
    MatInputModule,
    MatIconModule,
    MatCardModule,
    MatButtonModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }


Now if you run the application you'll be able to see the default data being populated in the application form.


Unit Testing ngOnit


When we created our Angular component the corresponding test file was created by default. In our case, there will be a `home.component.spec.ts` file.


You can try running the unit test for the Angular project using `ng test` which should run the unit test with some errors. Since our `home.component.ts` is using some Angular material UI imports, those need to be imported in the test file too.


Here is how the `home.component.spec.ts` file looks with the required imports inside the `beforeEach` block.

typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

describe('HomeComponent', () => {
  let component: HomeComponent;
  let fixture: ComponentFixture<HomeComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [HomeComponent],
      imports: [BrowserAnimationsModule, ReactiveFormsModule, MatInputModule, MatFormFieldModule, MatCardModule]
    });
    fixture = TestBed.createComponent(HomeComponent);
    component = fixture.componentInstance;
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});


Now before we write out the unit test case, let's try to understand what is already there inside the `home.component.spec.ts` file.


describe block

This block is used to group the unit test cases. In this particular case, we have grouped the Home component test cases under `describe('HomeComponent')` block


beforeEach block

This block is executed before each unit test case is run. Inside this block, we are configuring the test bed with the required imports for the test case to run. You can also see that we are creating a new component instance each time


it block

This block defines the particular unit test case.


The default test case `it('should create'` tests whether the component loaded without any errors and it confirms it by checking if its value is true or not.


Now let's write our test case for checking if the default value is being in the `ngOnInit` hook. Start by writing the `it` block.

  it('should set default values', () => {
    
  })


`HomeComponent` the default value is picked up from the variable `defaultData` so in the component instance let's set the `defaultData`.

typescript
component.defaultData = {
      firstName : "Tim",
      emailAddress : "[email protected]",
      phoneNumber : "+919895590754"
    }


Once the variable is set you need to run the Angular component change detection using the following command

typescript
fixture.detectChanges();


Now if all goes well then the Reactive form's value would be set as per the `defaultData` value. You can check by checking the value of the `profileForm` from the component instance.

typescript
expect(component.profileForm.getRawValue()['firstName']).toEqual(component.defaultData['firstName']);


Here is the complete unit test case:

typescript
  it('should set default values', () => {
    component.defaultData = {
      firstName : "Tim",
      emailAddress : "[email protected]",
      phoneNumber : "+919895590754"
    }
    fixture.detectChanges();
    expect(component.profileForm.getRawValue()['firstName']).toEqual(component.defaultData['firstName']);
  })


Now before running the test case, delete the spec file`app.component.spec.ts`. That’s the spec file for `AppComponent` with boilerplate code. Try running the unit test cases using `ng test` and it should return a successful result.


Adding Validation to Form


Currently, the form doesn't have any validations as such. We'll just add the required field validation to our form. Inside `ngOnInit` where you have defined the `profileForm` add required validation to each of the form fields.


Here is the modified `home.component.ts` file:

typescript
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css'],
})
export class HomeComponent {
  defaultData : any = {
    firstName : "Roy",
    emailAddress : "[email protected]",
    phoneNumber : "+919895590754"
  }
  profileForm :FormGroup= new FormGroup({});

  get f() { return this.profileForm.controls; }

  constructor(private _formBuilder: FormBuilder){}
  ngOnInit(): void {
    this.profileForm =  this._formBuilder.group({
      firstName: ['',Validators.required],
      emailAddress: ['', Validators.required],
      phoneNumber: ['', Validators.required]
    })
    this.profileForm.setValue({firstName : this.defaultData.firstName ,emailAddress : this.defaultData.emailAddress, phoneNumber : this.defaultData.phoneNumber})
  }
    onSubmit() {
    try{
      if(this.profileForm.valid){
        alert('Profile form is valid');
      } else {
        alert('Profile form invalid');
      }
    } catch(error){
    }
  }
}


Unit Testing Required Field Validation


Let's start by adding the `it` block.

  it('should fire required field validation for first name', () => {
    
  })


For the validation to fire the `firstName` field should have an empty value. So, let's use the `defaultData` to set empty values in the form.

  it('should fire required field validation for first name', () => {
    component.defaultData = {
      firstName : "",
      emailAddress : "[email protected]",
      phoneNumber : "+919895590754"
    }
    fixture.detectChanges();
  })


Now for the validation to fire we need to submit the button by calling the button click handler.

  it('should fire required field validation for first name', () => {
    component.defaultData = {
      firstName : "",
      emailAddress : "[email protected]",
      phoneNumber : "+919895590754"
    }
    fixture.detectChanges();
    component.onSubmit();
  })


When the button is clicked since the first name field is empty the profile form required field validation should fire. We can validate the required field validation using the same. 

component.profileForm.get('firstName')?.errors?.['required']


Here is what the complete test case looks like:

  it('should fire required field validation for first name', () => {
    component.defaultData = {
      firstName : "",
      emailAddress : "[email protected]",
      phoneNumber : "+919895590754"
    }
    fixture.detectChanges();
    component.onSubmit();
    expect(component.profileForm.get('firstName')?.errors?.['required']).toEqual(true);
  })


Similarly, you can write a unit test case for validating the required validation.


For validating email and phone number required validation set the default value as empty for the respective field.

typescript
  it('should fire required field validation for email', () => {
    component.defaultData = {
      firstName : "hello",
      emailAddress : "",
      phoneNumber : "+919895590754"
    }
    fixture.detectChanges();
    component.onSubmit();
    expect(component.profileForm.get('emailAddress')?.errors?.['required']).toEqual(true);
  })

  it('should fire required field validation for phone number, () => {
    component.defaultData = {
      firstName : "hello",
      emailAddress : "[email protected]",
      phoneNumber : ""
    }
    fixture.detectChanges();
    component.onSubmit();
    expect(component.profileForm.get('phoneNumber')?.errors?.['required']).toEqual(true);
  })


Save the above changes and try running the unit tests.

bash
ng test


And the above should return success.


Unit Testing Button Submit


On click of the save button if the form is valid it shows an alert with the message `Profile form is valid` and if the form is invalid message is `Profile form invalid`. Here is the button click handler,

typescript
  onSubmit() {
    try{
      if(this.profileForm.valid){
        alert('Profile form is valid');
      } else {
        alert('Profile form invalid');
      }
    } catch(error){
    }
  }


In the above code, we can write unit tests for two scenarios, one when the profile form is valid and once when it's invalid.


Let's first write a valid form. Here is the `it` block,

typescript
  it('should alert valid form', () => {
    
  })


Since we are testing valid forms it will happen only when the form has all the required field values. Let's set it using the `defaultData`.

typescript
  it('should alert valid form', () => {
    component.defaultData = {
      firstName : "hello",
      emailAddress : "[email protected]",
      phoneNumber : "+919895590754"
    }
    fixture.detectChanges();
  })


Now before submitting the button, we need to add a `spyOn` to check if the alert is getting fired or not. Jasmine provides us with a method called `spyOn` which we can use to monitor method calls. We'll be using `spyOn` to check if an alert is being called with our expected message.

typescript
  it('should alert valid form', () => {
    component.defaultData = {
      firstName : "hello",
      emailAddress : "[email protected]",
      phoneNumber : "+919895590754"
    }
    fixture.detectChanges();
    spyOn(window, "alert");
    component.onSubmit();
    expect(window.alert).toHaveBeenCalledWith("Profile form is valid")
  })


We added the `spyOn` before the submit button call. Now once the button is submitted we check if the alert was called with the expected message or not.


Similarly, for invalid forms, you need to set an empty value for `defaultData` and check for an alert to show an invalid form message.

typescript
  it('should alert invalid form', () => {
    component.defaultData = {
      firstName : "",
      emailAddress : "[email protected]",
      phoneNumber : "+919895590754"
    }
    fixture.detectChanges();
    spyOn(window, "alert");
    component.onSubmit();
    
    expect(window.alert).toHaveBeenCalledWith("Profile form invalid")
  })


Save the changes made and run the test cases.


Wrapping it up


In this Angular unit testing tutorial, we explored how to write unit test cases for Angular form code. A form can have many more features and as the form grows unit test case writing becomes a necessary tool to validate the correctness of code.

 


Jscrambler

The leader in client-side Web security. With Jscrambler, JavaScript applications become self-defensive and capable of detecting and blocking client-side attacks like Magecart.

View All Articles

Must read next

Web Development

Building User Registration Form With Reactive Forms in Angular 7

Learn how you can handle forms in Angular 7 web apps by leveraging Reactive Forms and post the reactive form to a REST API endpoint.

January 15, 2019 | By Jay Raj | 7 min read

Web Development

Handling CPU-Intensive Work Using Web Workers in Angular

Web workers are great for improving the performance of web applications. They can be used in Angular to handle CPU-intensive tasks.

August 6, 2020 | By Jay Raj | 6 min read

Section Divider

Subscribe to Our Newsletter