Developer’s Guide to Unit Testing in Angular — Part 2 (Services, Pipes, Directives)

Gurseerat Kaur
KhojChakra
Published in
6 min readAug 15, 2020

--

Explore how to write test cases for shared services, Http services, pipes, and attribute directives in Angular.

If you are new to unit testing and want to know how to start writing test cases, check out part 1 of this series, Developer’s guide to Unit Testing in Angular — Part 1.

In this part, we’ll be covering a few points:

  1. Testing Shared Services
  2. Testing Http Services
  3. Testing Pipes
  4. Testing Attribute Directives

Testing Shared Services

Shared services are used for sharing data between sibling components. Let’s create a shared service for users’ data, namely share-data-service.spec.ts.

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ShareDataService {
userData: BehaviorSubject<any> = new BehaviorSubject(undefined);
constructor() { }public setUser(user: any) {
this.userData.next(user);
}
public getUser(): Observable<any> {
return this.userData.asObservable();
}
}

Let’s create test cases for this simple shared service.

import { TestBed, async } from '@angular/core/testing';
import { ShareDataService } from './share-data.service';
describe('ShareDataService', () => {
let store: ShareDataService
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ ShareDataService ],
})
store = TestBed.get(ShareDataService);
});
it('should set User\'s data', async(() => {
const mockData = [{
"firstname": "Gurseerat",
"lastname": "Kaur",
"id": 1,
"dob": "11-02-1994"
}]

store.setUser(mockData);
store.getUser().subscribe(res => {
expect(res).toBe(mockData);
});
}));
});

Let’s see what is happening here:

  1. BehaviorSubject is used to emit the current value to the new subscribers.
  2. Configure a unit testing environment for your service using TestBed.
  3. Pass your mock data as an argument in the setUser() method and subscribe to the getUser() method expecting that the response will be the same as mock data.
  4. We’ll use async here for asynchronous tests.

Testing Http Services

Services are mostly used for fetching data using Http request and then components use them for displaying that data.

Suppose you have a service user.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class UserService { public apiUrl = 'path/to/your/api'; constructor(private http: HttpClient) { } // Http request with GET method
fetchUsers() {
return this.http.get<any>(this.apiUrl);
}
}

Let’s first set up our service as we have a dependency here on HttpClient in user.service.ts

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './web.service';
describe('UserService', () => {
let service: UserService;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ UserService ],
imports: [ HttpClientTestingModule ]
})
service = TestBed.get(UserService);
httpTestingController = TestBed.get(HttpTestingController);
});
afterEach(() => {
httpTestingController.verify();
});
});

Let’s break this down and try to understand what is happening over here:

  1. HttpClientTestingModule and HttpTestingController are used to mock the Http requests to the backend and flush those requests.
  2. We’ll add all the dependencies and providers in TestBed.
  3. verify() method of HttpTestingController instance will verify that no unmatched requests are outstanding and will throw an error indicating the requests that were not handled. We’ll add it in afterEach so that it is executed after every test case.

Let’s write test cases for fetchUsers() function.

beforeEach(() => {
const mockData = {
"status": "success",
"data": [{
"firstname": "Gurseerat",
"lastname": "Kaur",
"id": 1,
"dob": "11-02-1994"
},
{
"firstname": "John",
"lastname": "Doe",
"id": 2,
"dob": "22-09-1994"
}]
}
})
it('should check api response is VALID', () => {
service.fetchUsers().subscribe(res => {
expect(res).toEqual(mockData);
})
const req = httpTestingController.expectOne(service.apiUrl);
expect(req.request.method).toBe('GET');
expect(req.cancelled).toBeFalsy();
expect(req.request.responseType).toEqual('json');
req.flush(mockData);
})

Few points to note about what’s going on here:

  1. We’ve added mock data in beforeEach so that we can use the same variable in all the specs that follow in this describe block.
  2. Next, we’ll call fetchUsers() function, subscribe to its response, and expect the observable response is the same as mock data.
  3. We’ll then use HttpTestingController and expect that only one request was made to the API.
  4. We can then make any number of assertions. Here, we are expecting that the request method is GET, the request is not canceled and the response type is JSON.
  5. Next, we’ll complete the request by calling flush on the request and pass our mock data.

Note: While using beforeEach for variables, note that the variable will be available for all the assertions in that describe block and the describe blocks inside as well. For changing the value of mock data, you can create another describe block.

Testing Pipes

Writing test cases for the pipes is very easy in Angular. Let’s create a simple pipe, named file-name.pipe.ts, where you can get the file’s name from a file path.

import { Pipe, PipeTransform } from '@angular/core';@Pipe({
name: 'fileName'
})
export class FileNamePipe implements PipeTransform { transform(value: string): any {
let file = value.split('/');
let index = file.length - 1;
return file[index];
}
}

We’ll now create the test cases for this pipe.

import { FileNamePipe } from './file-name.pipe';describe('FileNamePipe', () => {
let pipe: FileNamePipe;
beforeEach(() => {
pipe = new FileNamePipe();
});
it('should get file name from file path', () => {
expect(pipe.transform('https://path/to/your/file/file.docx')).toBe('file.docx');
});
});

A few points to note here:

  1. Before each test spec is run, we create a new instance of FileName pipe and store in the pipe variable.
  2. As the pipes have only one function mostly, i.e., transform(), we’ll test it by passing some input in it and expect an output.

Testing Attribute Directives

An Attribute directive changes the appearance or behavior of a DOM element. Let’s create a simple directive for this that changes the color of the text.

import { Directive, ElementRef, Input, OnChanges } from '@angular/core';@Directive({
selector: '[changeColor]'
})
export class ChangeColorDirective implements OnChanges {
color = 'rgb(0, 0, 255)';
@Input('changeColor') changedColor;
constructor(private el: ElementRef) {
el.nativeElement.style.customProperty = true;
}
ngOnChanges() {
this.el.nativeElement.style.color = this.changedColor || this.color;
}
}

We can use this directive in our component like below:

<h1 changeColor>Default Color</h1>
<h2 changeColor="blue">Color Changed</h2>

Now, let’s first set up our change-color.directive.spec.ts file and resolve the dependencies.

import { ChangeColorDirective } from './change-color.directive';
import { TestBed, ComponentFixture, async } from '@angular/core/testing';
import { Component, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
describe('ChangeColorDirective', () => { let fixture: ComponentFixture<MockComponent>;
let directive: DebugElement[];
@Component({
template: `<h1 changeColor>Default Color</h1>
<h2>No Color Change</h2>
<h3 changeColor="red">Color Changed</h3>`
})
class MockComponent { }beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ MockComponent, ChangeColorDirective ]
})
fixture = TestBed.createComponent(MockComponent);
directive = fixture.debugElement.queryAll(By.directive(ChangeColorDirective));
fixture.autoDetectChanges();
})
})

Key points to note here:

  1. Create MockComponent where we’ll add the HTML template using our directive and write the test cases around it.
  2. We are fetching all the instances from our HTML template where the directive has been used by using By.directive.
  3. In case you are using debugElement.query instead of queryAll, make sure to change the directive type to DebugElement from DebugElement[].

Now, let’s write some test cases for our directive

it('should have 2 elements with changed color directive', async(() => {
expect(directive.length).toEqual(2);
}));
it('should change 1st element to default color', async(() => {
const dir = directive[0].injector.get(ChangeColorDirective);
const h1Color = directive[0].nativeElement.style.color;
expect(h1Color).toBe(dir.color);
}));
it('should not change color of 2nd element', async(() => {
const h2Color = fixture.debugElement.query(By.css('h2'));
expect(h2Color.properties.customProperty).toBeUndefined();
}));
it('should change 3rd element to defined color', async(() => {
const h3Color = directive[1].nativeElement.style.color;
expect(h3Color).toBe('red');
}));
  1. Now, here in the first test case, we are checking the number of instances where the directive is being used. In our MockComponent, we have two instances where we’ve added the changeColor directive.
  2. Next, we’ll inject the directive instance and get the default color. Since we are not defining any color in the changeColor attribute of <h1> tag, we’ll expect that the h1 has the default color.
  3. In the next test case, we’ll expect that custom property is undefined since we don’t have changeColor attribute for <h2> tag.
  4. At last, we’ll expect the color of the text in <h3> tag is the same as the color defined in the attribute, i.e, red.

--

--

Gurseerat Kaur
KhojChakra

Front End Developer | Part time coder, part time learner | Chicken Biryani fan