Technology

Jest Unit Test Framework

Configuration

Jest is pre-configured with Nx workspace. Packages that are related with jest are:

●jest (^25.1.0)

●ts-jest (^25.0.0)

●jest-preset-angular (^8.1.3)

●@nrwl/jest (^9.2.0)

●@types/jest (24.0.9)

In angular.json every application and library that is generated by Nx, has its architect section. Under architect, it has test section that defines its testing environment configuration.

"test": {         
	"builder": "@nrwl/jest:jest",         
	"options": {           
		"jestConfig": "libs/core/jest.config.js",           
		"tsConfig": "libs/core/tsconfig.spec.json",           
		"setupFile": "test-setup.ts"         
	}       
}

Basic Term

jest.config.js – this js file is configuration file for jest test cases, we have a global jest.config.js and application and libraries specific jest.config.js (which are subset of global jest.config.js)

setupFile – this file defines any extra setup required to run any specific test cases. Extra setup can be importing some external packages.

We have a global test-setup.ts and as well as applications and library specific test-setup.ts

Out of the box, other options for builder (@nrwl/jest) can be found in node_modules/@nrwl/jest/src/builders/jest/schema.json

How to run specific test case:

Define testMatch property in your (application|lib) jest.config.js with regex pattern value matching your test case file name.

testMatch: [‘**/+(user.service.)+(spec|test).+(ts|js)?(x)’]

Running Test Cases ->

npm test lib-name

How to Write Test Cases

For basic jest matchers please go through official jest website :
https://jestjs.io/docs/en/using-matchers

Pipes :

 import { blankIfNullPipe } from './blank-if-null.pipe';

 describe('blankIfNullPipe', () => {
 let pipe: blankIfNullPipe = null;
 beforeEach(() => {
    pipe = new blankIfNullPipe();
 }); 
	 
 it('create an instance', () => {  
    expect(pipe).toBeTruthy(); 
 }); 
	 
 it('convert null value to blank', () => {   
    expect(pipe.transform(null)).toMatch(' ');
 });
 }

Component :

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import {
    ButtonComponent
} from './button.component';
import {
    NO_ERRORS_SCHEMA,
    DebugElement,
    ChangeDetectorRef,
    ChangeDetectionStrategy
} from '@angular/core';
import {
    By
} from '@angular/platform-browser';
import {
    LocalizePipe
} from './localizePipe.pipe';
import {
    LocalizeService
} from './LocalizeService.service';
const languageServiceMock = {
    transform: jest.fn(() => {
        return 'abc';
    })
};
describe('ButtonComponent', () => {
  let component: ButtonComponent;
  let fixture: ComponentFixture < ButtonComponent > ;
  let el: DebugElement;
  beforeEach(async (done => {
    TestBed.configureTestingModule({
    declarations: [ButtonComponent, LocalizePipe],
      providers: [{
      provide: LocalizeService,
      useValue: localizeServiceMock
    }],
    schemas: [NO_ERRORS_SCHEMA]
  }) /* If ChangeDetectionStrategy -> OnPush (Approach 1)  */  .overrideComponent(ButtonComponent, {
  set: {
        changeDetection: ChangeDetectionStrategy.Default
       }
 }).compileComponents().then(() => {

     fixture = TestBed.createComponent(ButtonComponent);
     component = fixture.componentInstance;
  });
}));

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

it('should emit click event', () => {
    expect.hasAssertions();
    let clicked = false;
    component.clickButton.subscribe(() => {
        clicked = true;
    });
    component.click();
    expect(clicked).toBeTruthy();
});

it('should have width if width is 100%', () => {
    expect.hasAssertions();
    el = fixture.debugElement.query(By.css('button'));
    expect(el.styles['width']).toBeUndefined();
    fixture.detectChanges();
    expect(el.styles['width']).toBe('100%');
    component.width = '10%'; 

// If ChangeDetectionStrategy -> OnPush (Approach 2)   
const cd: ChangeDetectorRef = fixture.componentRef.injector.get(ChangeDetectorRef);   
cd.markForCheck();   
fixture.detectChanges();   
expect(el.styles['width']).toBe('10%');   

// If ChangeDetectionStrategy -> Default   

fixture.detectChanges();   
expect(el.styles['width']).toBe('10%'); });});

Service : That only holds data and doesn’t have any dependencies

import {
    TestBed
} from '@angular/core/testing';
import {
    UserService
} from './user.service';
import {
    IUser
} from '../interfaces';
import {
    User
} from '../classes';
const userJson: IUser = {
    firstName: 'Akash',
    lastName: 'Mishra',
    permission: [],
    userid: 1,
    role: ['Editor', 'Super Admin', 'Post Admin', 'Customer Support'],
    middleName: null,
    userName: 'akash.mishra@gmail.com',
    email: 'akash.mishra@gmail.com'
};
describe('UserService', () => {
    let service: UserService;
    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [UserService]
        });
        service = TestBed.inject(UserService);
        service.setUser(userJson);
    });
    it('should be created', () => {
        expect(service).toBeTruthy();
    });
    it('should have User object', () => {
        const isUserObject = service.getUser() instanceof User;
        expect(isUserObject).toBeTruthy();
    });
    it('should have permissions', () => {
        const user: User = service.getUser();
        expect(user.permissions.length).toBeGreaterThan(0);
    });
});

Service : That holds Business Logic along with Http Calls and have dependencies

import {
    TestBed,
    async
} from '@angular/core/testing';
import {
    ConfigurationService
} from './configuration.service';
import {
    HttpClientTestingModule
} from '@angular/common/http/testing';

import {
    CodeDecodePipe,
    GlobalValues,
    HttpService
} from './common';
import {
    of
} from 'rxjs';
const globalValues = {
    data: {
        obj: {
            customercare: {
                genericApi: {
                    genericApiIdentifier: 'mockapi'
                }
            }
        }
    }
};
const mockData = [{
        id: 10,
        name: 'Akash',
        configuration: {
            id: '1',
            name: 'Akash',
            shortname: 'Akash'
        },
        child: null,
        icon: null,
        description: null,
        uploadfile: null,
        parentId: null,
        iconName: null,
        enabled: true,
        hierarchy: null,
        createdBy: null,
        modifierby: null,
        creationTime: null
    }

];
const httpServiceMock = {
    sendGETRequest: jest.fn(),
    sendPOSTRequest: jest.fn()
};

describe('ConfigurationService', () => {
    let service: ConfigurationService;
    let httpService: HttpService;
    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [HttpClientTestingModule],
            providers: [HtmlDecodePipe, {
                provide: GlobalValues,
                useValue: globalValues
            }, {
                provide: HttpService,
                useValue: httpServiceMock
            }]
        });
        service = TestBed.inject(ConfigurationService);
        httpService = TestBed.inject(HttpService);
    });
    it('should be created', () => {
        expect(service).toBeTruthy();
    });
    it('should return root module data', async (() => {
        expect.hasAssertions();
        httpServiceMock.sendPOSTRequest.mockReturnValueOnce(of(mockData));
        service.getRootModule().subscribe(response => {
            expect(httpServiceMock.sendPOSTRequest.mock.calls.length).toBe(1);
            expect(response.length).toBe(1);
        });
    }));

});

Generate Code Coverage Report

Add collectCoverage property in your jest.config.js

collectCoverage: true

Coverage report generated at the location specified in your jest.config.js by property coverageDirectory

coverageDirectory: ‘./coverage/libs/core/tools’,

Open generated index.html of coverage for your lib, will be shown as below:

You can check specific file coverage by clicking on file name, and will be shown like :