ANGULARJS AND ANGULAR UNIT TESTING GUIDE WITH JASMINE FRAMEWORK FOR BUSY DEVELOPERS
Unit tests documentations can be overwhelming if you’re a busy developer looking for a way to test a specific functionality or a method. Like most of us developers, unit tests are a drag but we can’t deny their importance in our application's code. I was oblivious to them when I started working and had no idea about them. Step by step, with the help of our seniors and a lot of practice, their implementation became a casual day to day practice whenever a new functionality or a service is added. So, like promised in my blog, I decided to develop a small guide to share with you the necessary techniques when implementing unit tests if you’re a busy developer.
The first thing to know is that Angularjs and Angular put two types of tests at our disposal: unit tests, and End-to-End tests, which are actually functional tests. In this part, we will focus on unit tests.
we will be using Jasmine which is an extensible framework (produced by Pivotal ).
It includes everything needed to:
- define test suites (describe and it functions),
- implement assertions of any kind (expect function),
- quickly implement Spies/mocks (createSpy, createSpyObj and spyOn functions).
The test file structure (describe & beforeEach & it)
You only need to use the following 4 functions to keep your tests structured and well documented:
- describe: to define a sequence (or group) of "specs".
describe("list potato products", function() {
/* code */
});
- it to define a "spec" (or test).
describe('list potato products',() => {
it('should sort potato products by id',() => {});
});
- expect: to implement assertions. A range of verifiers (matchers) are available to do our verifications. In the following example, toEqual is used.
describe('list potato products', function() {
it('should sort potato products by id', function() {
const potatoIds= [2, 1, 3];
const sorted = sortUsers(potatoIds);
expect(sorted).toEqual([1, 2, 3]);
});
});
- beforeEach: inside of it we can add general mocks, variable, service injections or spies that will be used in multiple tests inside describe
describe('list potato products', function() {
beforeEach(inject(_service1_) => {
const potatoIds= [2, 1, 3];
});
it('should sort potato products by id', function() {
const sorted = sortUsers(potatoIds);
expect(sorted).toEqual([1, 2, 3]);
});
});
Matchers
Examples of matchers with Jasmine:
expect(booleanValue).toBe(true); expect(foo).toBeTruthy(); expect(foo).toBeFalsy(); expect(message).toMatch(‘some message’); expect(e).toBeLessThan(pi); expect(pi).toBeGreaterThan(e); expect(a.bar).toBeDefined(); expect(a.bar).toBeUndefined(); expect(user.name).toBeNull(); expect(a).toContain(‘bar’); expect(pi).toBeCloseTo(e, 0); expect(bar).toThrow(); expect(function).toHaveBeenCalled();
Reverse the condition with .not
.not can be added to reverse the expected result of a test.
expect(number).not.toEqual(2); expect(number). toEqual(0); expect(true). toBe(true); expect(false).not.toBe(true); expect(function).not.toHaveBeenCalled();
Test a service
With Angularjs
describe('list potato products',() => {
beforeEach(inject(_service1_, _service2_) => {
const potatoIds= [2, 1, 3];
//declare services after they have been injected above
let service1 = _service1_;
let service2 = _service2_;
});
//you can also inject a service directly into the it function
it('should sort potato products by id', inject((service4) => {
//access a service’s exported method to test it
const sorted = service1.sortUsers(potatoIds);
expect(sorted).toEqual([1, 2, 3]);
}));
});
With Angular >= 2
use testBed to inject Angular services into unit tests:
describe('list potato products',() => {
beforeEach(() => {
const potatoIds= [2, 1, 3];
let service = new ValueService();
service = testBed.get(ValueService);
});
it('should sort potato products by id', () => {
const sorted = service.sortUsers(potatoIds);
expect(sorted).toEqual([1, 2, 3]);
});
});
Creating Components
With Angularjs
The $compile service is used to render the directive.
describe('list potato products',() => {
beforeEach((_$compile_, _$scope_) => {
let $compile = _$compile_;
});
it('should sort potato products by id', function() {
// Compile a piece of HTML containing the directive
const element = $compile("/**/")($scope);
$scope.$apply();
expect(element.html()).toContain(‘some element’);
});
});
With Angular >= 2
The TestBed.createComponent method instantiates the component whose type is passed as a parameter and then returns a ComponentFixture object to control and inspect the component.
The main properties and methods of this class are:
- componentInstance: the instance of the BookPreviewComponent class,
- debugElement: object for inspecting and handling the DOM.
- detectChanges:triggers the Change Detection.
import ‘PotatoComponent’ from ‘./path’;
describe('list potato products',() => {
beforeEach((_$compile_, _$scope_) => {
const fixture = TestBed.createComponent(PotatoComponent);
const component = fixture.componentInstance;
const debugElement = fixture.debugElement;
});
it('should sort potato products by id', function() {
component.potato = new Potato({
id: 'potatos 1'
});
fixture.detectChanges();
expect(debugElement.nativeElement.querySelector('p').textContent).toEqual('potatos 1');
});
});
create a controller (Angularjs only)
It’s a good practice to declare the controller in beforeEach if you intend to use it in multiple tests to avoid duplications.
describe('list potato products',() => {
beforeEach((_$controller_) => {
$scope = {};
//declare the controller in before each or in the it clause
$controller = _$controller_;
controller = $controller('PotatosController', { $scope: $scope });
});
it('should sort potato products by id',() => {
const potatoIds= [2, 1, 3];
//use the $scope provided by the declared controller
$scope.sortUsers(potatoIds);
expect($scope.potatosList).toEqual([1, 2, 3]);
});
});
Mock methods
The spyOn method allows to track a service’s function behaviour like the value it returns, arguments…
const spiedMethod = spyOn(potatoService, 'getPotatosList'); ctrl.initComponent(); //initialise a component expect(spiedMethod).toHaveBeenCalled(); expect(spiedMethod).toHaveBeenCalledWith(‘argument1’, ‘argument2’);
Using the spyOn function by jasmine also can make any method return the value you want. Therefore you don’t need to worry about the inner working of a function and just force it to return whatever value to continue working on the test.
spyOn(potatoService, 'getPotatosList').and.returnValue(result);
CallThrough is used to spyOn a function and still call it
spyOn(potatoService, 'getPotatosList').and.callThrough();
Unit tests with Asynchronous methods
With Angularjs
You can spy on an asynchronous function and make it return a specific object.
it('should display potatos list when selecting a project',inject( ($q, potatosApi) => {
$controller('PotatosController', {$scope});
spyOn(potatosApi, 'getPotatosList').and.returnValue($q.when({
potatos: [
{id: 1, name: ‘potato 1’},
{id: 2, name: ‘potato 2’}
]
}));
$scope.selectPotato({id: '1'});
$rootScope.$digest();
expect($scope.selectedPotato.name).toEqual(‘potato 1’);
}));
In the example below we want to test the result returned by getPotatoStatus.
it('should check the potato’s status', inject((potatosApi) => {
const potato= {id: 1, name: ‘potato 1’};
potatosApi.getPotatoStatus(potato).then((result) => {
expect(result.status).toEqual(‘Raw potato’);
expect(result.label).toEqual(potato.name);
});
$rootScope.$digest();
}));
With Angular >= 2
The async utility tells Angular to run the code in a dedicated test zone that intercepts promises. The use of async with whenStable allows us to wait until all promises have been resolved to run our expectations.
it('should check the potato’s status', async(inject((potatosApi) => {
const potato= {id: 1, name: ‘potato 1’};
potatosApi.getPotatoStatus(potato).whenStable().then((result) => {
expect(result.status).toEqual(‘Raw potato’);
expect(result.label).toEqual(potato.name);
});
})));
keep it up ✌ ✌
Good job! keep it up Samer