Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Unit Testing in Angular

Tags:
Unit Testing In Angular

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.

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.



This post first appeared on Jscrambler, please read the originial post: here

Share the post

Unit Testing in Angular

×