Testing AngularJS with Typescript

A bit of advice

  • All unit test files should include a “should create” test, which simply checks for the component toBeTruthy(). This allows us to quickly check for failing initialization logic (constructor/onInit etc). If not, other tests might fail with no apparent reason (unrelated to their tested code).
  • Tests involving promises have to be async, and await for the response, else the expectations can fail since the code involves promise resolution. Most service tests are that way.

Actual tests

Components

See RolesList.spec.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import IModalService = angular.ui.bootstrap.IModalService;
import AppRoles = app.roles.AppRoles;
import RolesList = app.roles.RolesList;
import IPagedCriteria = lib.common.IPagedCriteria;
import IRoleCriteria = lib.common.IRoleCriteria;
import LookupService = lib.common.LookupService;
import RolesService = lib.common.roles.RolesService;

describe('RolesList', () => {

let component: RolesList;

// define the spies for mock objects
let roleServiceSpy: jasmine.SpyObj<RolesService>;
let uibModalSpy: jasmine.SpyObj<IModalService>;
let permissionServiceSpy: jasmine.SpyObj<PermissionService>;
let lookupServiceSpy: jasmine.SpyObj<LookupService>;

// dummy data for tests
const dummyString = '123';
const dummyLookupInfo = [{ code: '123', name: '123' }];
const dummyUser = {
administrator: true,
id: 'USER001',
name: 'Mr Hofer'
};
const dummyStateParam = { userAdministration: 1 };
const emptyPageRole = {
totalSize: 0,
pageInfo: {
offset: 0,
limit: 10,
pageNumber: 1
},
elements: [],
noaOrganisationId: '1'
};
const dummyBoolean = false;
const dummyRole: IRole = { id: 1, username: 'Mr Hofer' };

// initialize the module with angular mock
beforeEach(() => angular.mock.module(AppRoles.NAME));

// prepare and inject the mockers
beforeEach(inject(_$componentController_ => {

//initialize the mocks
roleServiceSpy = jasmine.createSpyObj('RoleService',
['searchRoles', 'searchRolesForAdmin']);
uibModalSpy = jasmine.createSpyObj('$uibModal', ['open']);
permissionServiceSpy = jasmine.createSpyObj(
'PermissionService',
['canManageRoleAssignment', 'canRequestRole', 'canViewMyRoles',
'canViewUsersRolesAndRequests']
);
lookupServiceSpy = jasmine.createSpyObj('LookupService', ['loadData']);

//mock functions
permissionServiceSpy.canManageRoleAssignment.and.returnValue(dummyBoolean);
permissionServiceSpy.canRequestRole.and.returnValue(dummyBoolean);
permissionServiceSpy.canViewMyRoles.and.returnValue(dummyBoolean);
permissionServiceSpy.canViewUsersRolesAndRequests.and.returnValue(dummyBoolean);
lookupServiceSpy.loadData.and.returnValue(Promise.resolve(dummyLookupInfo));
roleServiceSpy.searchRolesForAdmin.and.returnValue(
Promise.resolve(emptyPageRole));

// get ready the dummy angular componenet
const providers = {
PAGE_SIZE: 10,
[RolesService.ID]: roleServiceSpy,
$uibModal: uibModalSpy,
$stateParams: dummyStateParam,
[PermissionService.ID]: permissionServiceSpy,
[LookupService.ID]: lookupServiceSpy
};

const bindings = {
user: dummyUser,
administrator: true
};

component = _$componentController_(RolesList.ID, providers, bindings);
}));

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

it('should open amend role modal', () => {
uibModalSpy.open.and.returnValue({ result: Promise.resolve([]) });
component.openAmendRole(dummyRole);
expect(uibModalSpy.open).toHaveBeenCalled();
});

it('should check if role is in jurisdiction', () => {
const result = component.isRoleInJurisdiction(dummyRole);
expect(result).toBeFalsy();
});

it('should open update status modal', () => {
uibModalSpy.open.and.returnValue({ result: Promise.resolve([]) });
component.openUpdateStatus(dummyString);
expect(uibModalSpy.open).toHaveBeenCalled();
});

it('should open assign role modal', () => {
uibModalSpy.open.and.returnValue({ result: Promise.resolve([]) });
component.openAssignRole();
expect(uibModalSpy.open).toHaveBeenCalled();
});

it('should empty criteria', () => {
component.reset();
expect(component.searchCriteria).toEqual({});
expect(component.queryString).toEqual('');
expect(component.dateSelection).toBeNull();
});

it('should search roles', () => {
component.searchCriteria = {
paging: '0,10',
sorting: ''
};
const filter: IPagedCriteria<IRoleCriteria> =
{ idNumber: '2019-000111-00-11' };
roleServiceSpy.searchRolesForAdmin.and.returnValue(
Promise.resolve(emptyPageRole));
roleServiceSpy.searchRolesForAdmin.calls.reset();

component.searchRoles(filter);
expect(roleServiceSpy.searchRolesForAdmin).toHaveBeenCalledWith(
{ ...filter, paging: '0,10', sorting: '' });
});

it('should select roles', () => {
// simulate the mouse event
const event: MouseEvent = {
target: {
checked: true
}
} as unknown as MouseEvent;
component.toggleSelection(event, 0);

//check the reaction
expect(component.rolesToChange.length).toBe(1);
});

it('should return a sorting icon', () => {
component.getSortingIcon('');
expect(component.getSortingIcon('').length).toBeGreaterThan(0);
});

it('sortBy have been called', () => {
roleServiceSpy.searchRolesForAdmin.and.returnValue(
Promise.resolve(emptyPageRole));
component.sortBy(dummyString);
expect(roleServiceSpy.searchRolesForAdmin).toHaveBeenCalled();
});
});

Modals

See AssignRoleModal.spec.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import AssignRoleModal = app.roles.AssignRoleModal;
import ValidationErrorRegistry = lib.common.ValidationErrorRegistry;
import LookupInfo = lib.common.LookupInfo;

describe('AssignRoleModal', () => {
// what we will test
let modal: AssignRoleModal;

// mock
let uibModalSpy: jasmine.SpyObj<IModalService>;
let uibModalInstanceSpy: jasmine.SpyObj<IModalInstanceService>;
let rolesServiceSpy: jasmine.SpyObj<RolesService>;
let lookupServiceSpy: jasmine.SpyObj<LookupService>;
let permissionServiceSpy: jasmine.SpyObj<PermissionService>;
const resolveUser: IUser = {} as IUser;
let validationErrorRegistrySpy: jasmine.SpyObj<ValidationErrorRegistry>;

//dummies
const dummyStringList = ['123'];
const dummyNumber = 1;
const dummyLookupInfo: LookupInfo[] = [{ code: '123', name: '123' }];
const dummyRole: IRole = { id: 1, username: 'Mr Hofer' };

beforeEach(() => {

// init mocks
uibModalSpy = jasmine.createSpyObj('$uibModal', ['open']);
uibModalInstanceSpy = jasmine.createSpyObj('$uibModalInstance',
['close']);
rolesServiceSpy = jasmine.createSpyObj('RolesService',
['validateRolesForAdmin']);
lookupServiceSpy = jasmine.createSpyObj('LookupService', ['loadData']);
permissionServiceSpy = jasmine.createSpyObj('PermissionService',
['canAllocateTrialsToUsers']);
validationErrorRegistrySpy = jasmine.createSpyObj('ValidationErrorRegistry',
['addAll', 'getMessage', 'resetAll']);

// mock functions
lookupServiceSpy.loadData.and.returnValue(Promise.resolve(dummyLookupInfo));
permissionServiceSpy.canAllocateTrialsToUsers.and.returnValue(true);

// initialize instance to test
modal = new AssignRoleModal(
uibModalSpy,
uibModalInstanceSpy,
rolesServiceSpy,
resolveUser,
lookupServiceSpy,
permissionServiceSpy,
validationErrorRegistrySpy
);
});

// constructor test
it('should create', () => {
expect(modal).toBeTruthy();
});

// async test, as it uses a Promise
it('validateRoles should validate roles', async () => {
// mock the Promise
rolesServiceSpy.validateRolesForAdmin.and.returnValue(Promise.resolve([]));

// wait for async response
await modal.validateRoles();

expect(rolesServiceSpy.validateRolesForAdmin).toHaveBeenCalled();
expect(uibModalInstanceSpy.close).toHaveBeenCalled();
});

it('assignAnotherRole should add a role', () => {
modal.assignAnotherRole();
expect(modal.rolesToSave.length).toBe(2);
});

it('openOrganisation should open a modal', () => {
uibModalSpy.open.and.returnValue({ result: Promise.resolve(
{ organisation: { name: '123', businessKey: '123' } }) });
modal.openOrganisation(dummyRole);
expect(uibModalSpy.open).toHaveBeenCalled();
});

it('getError should retrieve errors', () => {
validationErrorRegistrySpy.getMessage.and.returnValue(dummyStringList);
const result = modal.getError(dummyNumber);
expect(result).toEqual(dummyStringList);
});

it('deleteRole should remove an item from list', () => {
modal.rolesToSave = [dummyRole];
modal.deleteRole(0);
expect(modal.rolesToSave.length).toBe(0);
});

it('onFromDateChange should change min date', () => {
modal.toDateOptions = [{ minDate: new Date() }];
modal.onFromDateChange('2019-01-01', 0);
expect(modal.toDateOptions[0].minDate.toISOString())
.toEqual(moment('2019-01-01').toISOString());
});
});

Service

see RolesService.spec.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import IRoleCriteria = lib.common.IRoleCriteria;
import RolesService = lib.common.roles.RolesService;

describe('RolesService', () => {

let service: RolesService;

let rolesApiSpy: jasmine.SpyObj<RolesApi>;

const dummyRoles: IRole[] = [{ id: 1, username: 'Mr Hofer' }];
const dummyCriteria: IPagedCriteria<IRoleCriteria> = {
username: 'Mr Hofer',
email: 'hofer@dummymail.com',
organisationName: 'dummys.inc',
organisationId: '01',
idNumber: '123456789',
status: 'happy',
dateLastSevenDays: true,
dateThisMonth: true,
dateFrom: '01-01-2001',
dateTo: '01-01-2048',
noaOrganisationId: '01',
paging: '0'
};
const emptyPageData: IPagedResults<IRole> = {
totalSize: 0,
pageInfo: {
offset: 0,
limit: 10,
pageNumber: 1
},
elements: []
};
const validatedRoles: IRoleValidationResult[] = [{ data: dummyRoles[0],
roleValid: true, violations: [] }];

beforeEach(() => angular.mock.module(RolesModule.NAME));

beforeEach(() => {
rolesApiSpy = jasmine.createSpyObj(
'RolesApi',
['saveRoles', 'saveRolesForAdmin', 'searchRoles', 'searchRolesForAdmin',
'validateRoles', 'validateRolesForAdmin']
);

const providers = {
[RolesApi.ID]: rolesApiSpy
};

angular.mock.module(($provide: IProvideService) => Object.entries(providers)
.forEach(([key, value]) => $provide.value(key, value)));

inject(($injector: IInjectorService) => service = $injector
.get(RolesService.ID));
});

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

it('should save user roles', async () => {
rolesApiSpy.saveRoles.and.returnValue(Promise.resolve(
{ data: { data: dummyRoles, violations: [] } }));

const response = await service.saveRoles(dummyRoles);

expect(rolesApiSpy.saveRoles).toHaveBeenCalledWith(dummyRoles);
expect(response).toEqual({ data: dummyRoles, violations: [] });
});

it('should save user roles with admin permissions', async () => {
rolesApiSpy.saveRolesForAdmin.and.returnValue(Promise.resolve(
{ data: { data: dummyRoles, violations: [] } }));

const response = await service.saveRolesForAdmin(dummyRoles);

expect(rolesApiSpy.saveRolesForAdmin).toHaveBeenCalledWith(dummyRoles);
expect(response).toEqual({ data: dummyRoles, violations: [] });
});

it('should find roles given a bunch of criteria', async () => {
rolesApiSpy.searchRoles.and.returnValue(Promise.resolve(
{ data: emptyPageData }));

const response = await service.searchRoles(dummyCriteria);

expect(rolesApiSpy.searchRoles).toHaveBeenCalledWith(dummyCriteria);
expect(response).toEqual(emptyPageData);
});

it('should find roles given a bunch of criteria', async () => {
rolesApiSpy.searchRolesForAdmin.and.returnValue(Promise.resolve(
{ data: emptyPageData }));

const response = await service.searchRolesForAdmin(dummyCriteria);

expect(rolesApiSpy.searchRolesForAdmin).toHaveBeenCalledWith(dummyCriteria);
expect(response).toEqual(emptyPageData);
});

it('should check the user roles sent as parameter', async () => {
rolesApiSpy.validateRoles.and.returnValue(Promise.resolve(
{ data: validatedRoles }));

const response = await service.validateRoles(dummyRoles);

expect(rolesApiSpy.validateRoles).toHaveBeenCalledWith(dummyRoles);
expect(response).toEqual(validatedRoles);
});

it('should check the user roles sent as parameter', async () => {
rolesApiSpy.validateRolesForAdmin.and.returnValue(Promise.resolve(
{ data: validatedRoles }));

const response = await service.validateRolesForAdmin(dummyRoles);

expect(rolesApiSpy.validateRolesForAdmin).toHaveBeenCalledWith(dummyRoles);
expect(response).toEqual(validatedRoles);
});
});

Filters

See StartsWith.spec.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import StartsWith = lib.common.StartsWith;
import LibCommon = lib.common.LibCommon;

describe('StartsWith', () => {

let filter: Function;
const sourceList: any[] = [
{ code: 1, description: 'Subject' },
{ code: 2, description: 'Something' },
{ code: 3, description: 'Something Else' }
];
const comparisonPropertyStringType = 'description';
const comparisonPropertyNumberType = 'code';

beforeEach(() => angular.mock.module(LibCommon.NAME));

beforeEach(inject((_$filter_) => filter = _$filter_(StartsWith.ID)));

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

it('should return multiple results when matches exist, comparator property passed',
() => { expect(filter(sourceList, 'Some', comparisonPropertyStringType).length)
.toEqual(2);
});

it('should return no matches for type number', () => {
expect(filter(sourceList, 123).length).toEqual(0);
});

it('should return no matches for type boolean', () => {
expect(filter(sourceList, false).length).toEqual(0);
});

it('should return no matches for null', () => {
expect(filter(sourceList, null).length).toEqual(0);
});

it('should return multiple results when matches exist', () => {
expect(filter(sourceList, 'Some').length).toEqual(2);
});

it('should return a starts with match from the array', () => {
expect(filter(sourceList, 'Sub', comparisonPropertyStringType))
.toEqual([{ code: 1, description: 'Subject' }]);
});

it('should return a starts with match from the array, comparator property passed',
() => { expect(filter(sourceList, 'Sub', comparisonPropertyStringType))
.toEqual([{ code: 1, description: 'Subject' }]);
});

it('should not return a starts with match from the array,
comparison on numeric value property', () => {
expect(filter(sourceList, 'Sub', comparisonPropertyNumberType).length).toEqual(0);
});

});