Prerequisites

  • Solid unit tests for your code, E2E tests.
  • next-update: An npm package which tests whether your dependencies can be updated without breaking the tests.
    • It will find all new packages
    • It updates the packages in sequence
    • It keep the update if your tests pass

How to

On CLI

  1. Install it globally.

    1
    npm install -g next-update 
  2. Run it.

    1
    next-update

On project

  1. Install next update

    1
    npm install next-update --save-dev
  2. Configure the script on package json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    "name": "a-sample-node-project",
    "version": "0.0.1",
    "description": "A sample node project",
    "scripts": {
    "test": "karma start",
    "start": "node app.js",
    "dep:update": "next-update" // Configure an npm script
    },
    "devDependencies": {
    "next-update": "^3.6.0"
    },
    "dependencies": {
    },
    }
  3. Run the script

    1
    npm run dep:update

Mocking the unmockable

Most of these are not recommendable practices. Powermock is being replaced by Mockito 2.0, and the reflection tricks should be avoided.
However we may happen to need them due to working with legacy code.

Let’s suppose we have taht kind of class with several issues such as this excerpt:

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
public class CTProcessConfig implements ProcessEngineLookup {
private static final String TENANT = "TENANT";

// a private variable we can not modify as there is no setter
private String privateName = "DUMMY_SERVICE";

private Map<String, ProcessEngine> storage;
private static DataSource datasource;

public CTProcessConfig() {
storage = new HashMap<>();
initializeStorage();
}

// this is an static method
public static DataSource getDatasource() {
if (datasource == null) {
try {
// this is a constructor call, we can't mock it with Mockito
datasource = (DataSource) new InitialContext().lookup(
PROCESS_ENGINE_JNDI_SOURCE);
} catch (NamingException e) {
throw new IOException("Unable to construct the datasource", e);
}
}
return datasource;
}

//this is a private method which can't be tested on its own
@Override
private TransactionManager lookupTransactionManager() {
try {
// this is an static method call, we can't mock it with Mockito
return InitialContext
.doLookup(WEB_LOGIC_TRANSACTION_MANAGER_BEAN_NAME);
} catch (NamingException e) {
throw ClinicalTrialException
.create("Unable to lookup transaction manager", e);
}
}
}

PowerMock dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${version.org.mockito}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${version.junit}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${version.powermock}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>${version.powermock}</version>
<scope>test</scope>
</dependency>

How to mock the static

We have the InitialContext class. We should use Powermock in order to do that.

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
// use the powermock runner
// do not forget to add the problematic static class on prepare with
@RunWith(PowerMockRunner.class)
@PrepareForTest({ InitialContext.class })
public class CTProcessConfigTest {

// a private variable we can not modify
private String privateName = "DUMMY_SERVICE";

// instance to test
private CTProcessConfig instance;

// Mockito mocks
@Mock
private InitialContext initialContext;
@Mock
private TransactionManager transactionManager;
@Mock
private Datasource datasource;

@Before
public void setUp() throws NamingException {
initInitialContext();
// initialize your instance for tests
instance = new CTProcessMultiengineConfig();
}

private void initInitialContext() throws NamingException {
// mock the static
PowerMockito.mockStatic(InitialContext.class);
// treat it like a normal mockito mock
Mockito.when(InitialContext.doLookup(Matchers.anyString()))
.thenReturn(transactionManager);
}
}

How to test a private method

This is not a recommended, but there are cases when private methods are overwritten, so they are not easy to test individually. Let’s suppose we want to test the lookupTransactionManager method on its own. We can’t as it is private, hence we can use reflection to get it, make it accesible, and then set it back to its original state.

1
2
3
4
5
6
7
8
@Test
public void lookupTransactionManager_validResult(){
Method method = instance.getClass()
.getDeclaredMethod("lookupTransactionManager");
method.setAccessible(true);
Assert.asserEquals(transactionManager, method.invoke(instance));
method.setAccessible(false);
}

You may generalize the process in a similar way.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Object invokeMethodByName(Object instance, String nameOfMethod,
List<Class> parametersClass, List<Object> parametersValues)
throws InvocationTargetException {
Object result = null;
try {
Method method = instance.getClass().getDeclaredMethod(nameOfMethod,
parametersClass.toArray(new Class[]{}));
method.setAccessible(true);
result = method.invoke(instance, parametersValues.toArray());
method.setAccessible(false);
} catch (IllegalArgumentException | NoSuchMethodException |
IllegalAccessException | InvocationTargetException e) {
LOG.log(Level.SEVERE, null, e);
}
return result;
}

How to change the value of a private variable

Let’s suppose we need to modify an attribute value, but we have no setter available.

1
private String privateName = "DUMMY_SERVICE";

We may use reflection once again.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Before
public void setUp(){
Field field = instance.getClass().getDeclaredField("name");
setValueToPrivateField(instance, field, "NEW_NAME");
}

private void setValueToPrivateField(Object object, Field privateField,
Object newFieldValue) throws IllegalArgumentException, IllegalAccessException,
SecurityException {
privateField.setAccessible(true);
privateField.set(object, newFieldValue);
privateField.setAccessible(false);
}

How to mock a constructor call

Be careful as your coverage metter may not work as expected, but if you debug the code you will see it is working as expected, covering the code.

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
// use the Powermock runner
// add the problematic static class and the current class where the "new" happens
@RunWith(PowerMockRunner.class)
@PrepareForTest({ InitialContext.class, CTProcessConfig.class })
public class CTProcessConfigTest {

// instance to test
private CTProcessMultiengineConfig instance;

// Mockito mocks
@Mock
private InitialContext initialContext;
@Mock
private TransactionManager transactionManager;
@Mock
private Datasource datasource;

@Before
public void setUp() throws NamingException {
initInitialContext();
// initialize your instance for tests
instance = new CTProcessConfig();
}

private void initInitialContext() throws NamingException {
// mock the constructor, check the arguments
PowerMockito.whenNew(InitialContext.class).withNoArguments()
.thenReturn(initialContext);
// regular Mockito code
Mockito.when(lookUp(Matchers.anyString())).thenReturn(datasource);
}

@Test
public void getDatasource(){
Assert.assertEquals(datasource, instance.getDatasource());
}
}

How to mock a transaction lock on an EntityManager

Let’s suppose we have to test the following code, with JPA QueryDSL:

1
2
3
4
5
6
7
8
9
10
11
12
private EntityManager em;
private root;

@Override
public long unlockByLockedBy(@Nonnull final String lockedBy) {
final QUnionControlReport root = QUnionControlReport.unionControlReport;
return new JPAUpdateClause(entityManager, root)
.where(root.lockedBy.equalsIgnoreCase(lockedBy))
.setNull(root.lockedBy)
.setNull(root.lockDateTime)
.execute();
}

We need the transaction to get engaged, so we would only need to begin a transaction, and and not forget to finish it to avoid getting stuck.

1
2
3
4
5
6
7
8
9
/**
* Test unlockByLockedBy
*/
@Test
public void unlockByLockedBy() {
em.getTransaction().begin();
assertNotNull(instance.unlockByLockedBy("lockedBy"));
em.getTransaction().commit();
}

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);
});

});

Basic overview

JavaScript has primitives, objects and functions. All of them are values. All are treated as objects, even primitives.

Primitives

Number, boolean, string, undefined and null are primitives.

Number

  • Numbers inherit methods from the Number.prototype object. Methods can be called on numbers:

    1
    2
    (123).toString();  //'123'
    (1.23).toFixed(1); //'1.2'
  • Global functions for converting to numbers : parseInt(), parseFloat() and Number():

    1
    2
    3
    4
    5
    parseInt('1')       //1
    parseInt('text') //NaN
    parseFloat('1.234') //1.234
    Number('1') //1
    Number('1.234') //1.234
  • Invalid arithmetic operations or invalid conversions will not throw an exception, but will result in the NaN “Not-a-Number” value. isNaN() can detect NaN.

  • The + operator can add or concatenate.

    1
    2
    3
    1 + 1      //2
    '1' + '1' //'11'
    1 + '1' //'11'

String

A string stores a series of Unicode characters. The text can be inside double quotes “” or single quotes ‘’.

  • Strings have methods like : substring(), indexOf() and concat() .

    1
    2
    3
    'text'.substring(1,3) //ex
    'text'.indexOf('x') //2
    'text'.concat(" end") //text end
  • (ES6) You can pad the current String with another String via String.prototype.padStartand String.prototype.padEnd

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // padStart
    const cardNumber = '1234567812345678';
    const last3Digit = cardNumber.slice(-3);
    const maskedCardNumber = last3Digit.padStart(16, 'X');
    console.log(maskedCardNumber); // Output: "XXXXXXXXXXXXX678"
    // padEnd
    const loremIpsum = 'Lorem Ipsum is simply dummy text of the printing and';
    const loremIpsumWithDots = loremIpsum.padEnd(loremIpsum.length+3, '.');
    console.log(loremIpsumWithDots);
    // Output: Lorem Ipsum is simply dummy text of the printing and...
  • (ES6) String interpolation can also be achieved with ${nameVar}

    1
    2
    3
    4
    5
    const name = 'Angeles';
    const hobby = 'listening to music';
    const sayHello = 'Hello, my name is ${name}. My hobby is ${hobby}.';
    console.log(sayHello);
    // Output: Hello, my name is Angeles. My hobby is listening to music.
  • Strings, like all primitives, are immutable. For example concat() doesn’t modify the existing string but creates a new one.

Boolean

A boolean has two values : true and false.

  • The language has truthy and falsy values.

    • false, null, undefined, (empty string), 0 and NaN are falsy.
    • All other values, including all objects, are truthy.
  • Evaluation:

    • Truthy value is evaluated to true when executed in a boolean context.
    • Falsy value is evaluated to false.
      1
      2
      3
      4
      5
      6
      let text = '';
      if(text) {
      console.log('This is true');
      } else {
      console.log('This is false');
      }

Objects

  • An object is a dynamic collection of key-value pairs.

    • key: string
    • value: primitive, object, or function
  • Create an object

    1
    2
    3
    4
    let obj = {
    message : 'A message',
    doSomething : function() {}
    }
  • Manage properties:

    1
    2
    3
    4
    let obj = {}; //create empty object
    obj.message = 'A message'; //add property
    obj.message = 'A new message'; //edit property
    delete object.message; //delete property
  • Objects are hash-maps. A simple hash-map can be created using `Object.create(null)`` :

    1
    2
    3
    4
    let spanish = Object.create(null);
    spanish['yes'] = 'si';
    spanish['no'] = 'no';
    spanish['yes']; //'si'
  • Immutable object: use Object.freeze() .
    Object.keys() can be used to iterate over all properties.

    1
    2
    3
    4
    5
    6
    function logProperty(name){
    console.log(name); //property name
    console.log(obj[name]); //property value
    }

    Object.keys(obj).forEach(logProperty);
  • (ES6) Iterate objects: use Object.values for direct access and Object.entries for enumerables

    1
    2
    3
    4
    5
    6
    7
    8
    const objectDummy = {
    name: 'dummy',
    isAvailable: false
    };
    Object.values(objectDummy);
    // Output: ['dummy', false]
    Object.entries(objectFoo);
    // Output: [['name', 'dummy'], ['isAvailable', false]]

Primitives vs. Objects

  • Primitives are treated like objects, in the sense that they have methods but they are not objects.
  • Primitives are immutable
  • Objects are mutable.

Variables

  • var
    • declares and optionally initializes a variable
    • default value = undefined
    • have a function scope
  • let
    • has block scope
  • const
    • cannot be reassigned (constructor)
    • its value can still be mutable via set properties
    • has a block scope

Notes:

  • const freezes the variable
  • Object.freeze() freezes the object
  • The scope of a variable declared outside any function is global.

Array

JavaScript has array-like objects. An array is implemented using an object.

  • Elements are accessed using their indices
  • Indices are converted to strings and used as names for retrieving values
  • Removing values from the array with delete will leave holes. splice() can be used to avoid the problem, but it can be slow.
    1
    2
    3
    4
    let arr = ['A', 'B', 'C'];
    delete arr[1];
    console.log(arr); // ['A', empty, 'C']
    console.log(arr.length); // 3
  • (ES6) You can check the content with Array.prototype.includes
    1
    2
    3
    4
    5
    [1, 2].includes(1); // true
    [1, 3].includes(2); // false
    var foo = 'var';
    foo.includes('v'); // true
    foo.includes('V'); // false
  • Note: Stack and queue can easily be implemented using the array methods
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let stack = [];
    stack.push(1); // [1]
    stack.push(2); // [1, 2]
    let last = stack.pop(); // [1]
    console.log(last); // 2

    let queue = [];
    queue.push(1); // [1]
    queue.push(2); // [1, 2]
    let first = queue.shift();//[2]
    console.log(first); // 1

Functions

Definition

  • Objects which represent independent units of behavior.
  • Functions can be:
    • assigned to variables
    • stored in objects or arrays
    • passed as an argument to other functions
    • returned from functions.

There are three ways to define a function:

  • Function Declaration (aka Function Statement)
    • function is the first keyword on the line
    • it must have a name
    • it can be used before definition. Function declarations are moved, or “hoisted”, to the top of their scope.
      1
      function doSomething() {}
  • Function Expression (aka Function Literal)
    • function is not the first keyword on the line
    • the name is optional (anonymous function expression)
    • it needs to be defined, then it can execute
    • it can auto-execute after definition (called “IIFE” Immediately Invoked Function Expression)
      1
      let doSomething = function() {}
  • Arrow Function
    • creates an anonymous function expression
    • Arrow functions don’t have their own this and arguments
      1
      let doSomething = () = > {};

Invocation

  • Function form
    1
    doSomething(arguments)
  • Method form
    1
    2
    theObject.doSomething(arguments)
    theObject['doSomething'](arguments)
  • Constructor form
    1
    new doSomething(arguments)
  • Apply form
    1
    2
    doSomething.apply(theObject, [arguments])
    doSomething.call(theObject, arguments)
  • Bind form
    1
    2
    let doSomethingWithObject = doSomething.bind(theObject);
    doSomethingWithObject();
    Functions can be invoked with more or fewer arguments than declared in the definition. The extra arguments will be ignored, and the missing parameters will be set to undefined.

Function pseudo-parameters

  • this
    • represents the function’s contex
    • only functions defined with the function keyword have their own this contex
    • its value depends on how the function was invoked
      1
      2
      3
      function doSomething(){
      console.log(this)
      }
Form this
Function window/undefined
Method theObject
Constructor the new object
apply theObject
bind theObject
  • arguments

    • The arguments pseudo-parameter gives all the arguments used at invocation.
    • It’s an array-like object, but not an array. It lacks the array methods.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function reduceToSum(total, value){
      return total + value;
      }

      function sum(){
      let args = Array.prototype.slice.call(arguments);
      return args.reduce(reduceToSum, 0);
      }

      sum(1,2,3);
    • An alternative is the new rest parameters syntax. This time args is an array object.
      1
      2
      3
      4
      function sum(...args){
      return args.reduce(reduceToSum, 0);
      }
      return;
    • A function with no return statement returns undefined. Pay attention to the automatic semi-colon insertion when using return. The following function will not return an empty object, but rather an undefined one. To avoid the issue, use { on the same line as return;
      1
      2
      3
      4
      function getObject(){ 
      return {}
      }
      getObject()

Dynamic Typing

  • JavaScript has dynamic typing

    1
    2
    3
    4
    5
    6
    7
    function log(value){
    console.log(value);
    }

    log(1);
    log('text');
    log({message : 'text'});
    • Values have types
    • Variables do not have types
    • Types can change at run time
  • The `typeof()`` operator can check the type of a variable.

    1
    2
    3
    4
    5
    6
    7
    8
    let n = 1;
    typeof(n); //number

    let s = 'text';
    typeof(s); //string

    let fn = function() {};
    typeof(fn); //function

A Single Thread

  • The main JavaScript runtime is single threaded.
  • 2 functions can’t run at the same time.
  • The runtime contains an Event Queue which stores a list of messages to be processed.
    • There are no race conditions, no deadlocks.
    • However, the code in the Event Queue needs to run fast. Otherwise the browser will become unresponsive and will ask to kill the task.

Exceptions

  • JavaScript sometimes has a preference for silent errors.
  • The next code will not throw an exception when I try to modify a frozen object:
    1
    2
    let obj = Object.freeze({});
    obj.message = 'text';
  • Strict mode eliminates some JavaScript silent errors. use strict; enables strict mode.

Prototypes

Prototype patterns

  • Types

    • Object.create()
    • Constructor function
    • class build objects over the prototype system
  • Build

    • classic
      1
      2
      3
      4
      5
      6
      7
      8
      9
      //the __proto__ property of `specializedService` points to the service object
      let service = {
      doSomething : function() {}
      }
      //build a service which has the service object as its prototype
      let specializedService = Object.create(service);
      console.log(specializedService.__proto__ === service); //true
      // it inherits the method
      specializedService.doSomething();
    • with Class
      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Service {
      doSomething(){}
      }

      class SpecializedService extends Service {
      }

      let specializedService = new SpecializedService();
      console.log(specializedService.__proto__ === SpecializedService.prototype);
      • all methods defined in the Service class will be added to the Service.prototype object
      • instances of the Service class will have the same prototype Service.prototype object
      • all instances will delegate method calls to the Service.prototype object
      • methods are defined once on Service.prototype and then inherited by all instances

Prototype chains

Objects inherit from other objects. Each object has a prototype and inherits their properties from it.

When you request a property which the object does not contain, JavaScript will look down the prototype chain until it either finds the requested property, or until it reaches the end of the chain.

Functional Patterns

JavaScript has first class functions and closures. These are concepts that open the way for Functional Programming in JavaScript. As a result, higher order functions are possible.

  • Closure is an inner function that has access to the parent function’s variables, even after the parent function has executed.
  • A higher order function is a function that:
    • takes another function as an input
    • returns a function
    • does both

Declaration

1
2
3
4
5
6
7
8
9
10
11
12
// classic declaration 
async function functionName (arguments) {
// Do something asynchronous
}
// arrow declaration
const functionNameAWithrrows = async (arguments) => {
// Do something asynchronous
}

// they always return promises
const promise = functionName(arguments);
console.log(promise); // Promise

Promises return something in the future:

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
getSomethingWithPromise()
.then(data => {/* do something with data */})
.catch(err => {/* handle the error */});

// promise return
const promise = new Promise((resolve, reject) => {
// Note: only 1 param allowed
return resolve(42);
})

// Parameter passed resolve would be the arguments passed into then.
promise.then(number => console.log(number)); // 42

// promise reject
const promise = new Promise((resolve, reject) => {
// Note: only 1 param allowed
return reject('💩');
})

// Parameter passed into reject would be the arguments passed into catch.
promise.catch(err => console.log(err)); // 💩

// full example
const ChellWantsCake = cakeType => {
return new Promise((resolve, reject) => {
setTimeout(()=> {
if (cakeType == 'Portal cake') {
resolve('You got a cake!');
} else {
reject('The cake is lie');
}
}, 1000)//timeout
})
}

Await

It lets you wait for the promise to resolve, once it finishes it returns the parameter passed into the then call.

1
2
3
4
5
6
const test = async _ => {
const one = await getOne() {
console.log(one); // 1
}
}
test();

Return await

There’s no need to await before returning a promise. You can return the promise directly.

(If you return await something, you resolve the original promise first. Then, you create a new promise from the resolved value. return await effectively does nothing. No need for the extra step).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Don’t need to do this 
const test = async _ => {
return await getOne();
}
test()
.then(value => {
console.log(value) // 1
});

// Do this instead
const test = async _ => {
return getOne();
}
test()
.then(value => {
console.log(value); // 1
});

If you don’t need await, you don’t need to use an async function. The example above can be rewritten as follows:

1
2
3
4
5
6
7
8
// Do this instead
const test = _ => {
return getOne();
}
test()
.then(value => {
console.log(value); // 1
})

Handling errors

Rejects can be handled with catch

1
2
3
4
5
6
7
8
const getOne = async (success = true) => { 
if (success){
return 1;
throw new Error(‘💩’);
}
}
getOne(false)
.catch(error => console.log(error)); // 💩

You can use use a try/catch calls

1
2
3
4
5
6
7
8
const test = async _ => {
try {
const one = await getOne(false);
} catch (error) {
console.log(error); // 💩
}
}
test();

If you have multiple await keywords, error handling can become ugly…

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
// You do not need to this
const test = async _ => {
try {
const one = await getOne(false);
} catch (error) {
console.log(error); // 💩
}
try {
const two = await getTwo(false);
} catch (error) {
console.log(error); // 💩
}
try {
const three = await getThree(false);
} catch (error) {
console.log(error); // 💩
}
}
test();

// Do this instead
const test = async _ => {
const one = await getOne(false);
const two = await getTwo(false);
const three = await getThree(false);
}
// get a single error, just one
test()
.catch(error => console.log(error));

Multiple awaits

await blocks JavaScript from executing the next line of code until a promise resolves -> slow down execution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// delaying a function a number of milliecons (ms)
const sleep = ms => {
return new Promise(resolve => setTimeout(resolve, ms));
}

console.log(‘Now’);
// using Sleep, we delay 1 second
sleep(1000)
.then(v => { console.log(‘After one second’) });
// console logs ‘Now’ immediately. One second later, it logs ‘After one second’

const getOne = _ => {
// sleep 1 second, then return "1"
return sleep(1000).then(v => 1);
}

If you await getOne(), you’ll see that it takes one second before getOne resolves.

1
2
3
4
5
6
7
8
const test = async _ => {
// console logs ‘Now’ immediately
console.log(‘Now’);
const one = await getOne();
// after one second, console logs 1
console.log(one);
}
test();

Let’s suppose we add 3 promises

  • You may wait for three seconds before all three promises get resolved

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const getOne = _ => {
    return sleep(1000).then(v => 1);
    };
    const getTwo = _ => {
    return sleep(1000).then(v => 2);
    };
    const getThree = _ => {
    return sleep(1000).then(v => 3);
    };
  • Or you may search all simultaneously

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const test = async _ => {
    // console shows ‘Now’ immediately
    console.log(‘Now’);
    const one = await getOne();
    console.log(one);
    const two = await getTwo();
    console.log(two);
    const three = await getThree
    // 'three' and 'done' appear at the same time
    console.log(three);
    console.log(‘Done’);
    };
    test();
  • All these promises may be fetched with Promise.all

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // create the three promises before this
    const test = async _ => {
    //add all three promises into an array
    const promises = [getOne(), getTwo(), getThree()];
    console.log(‘Now’);
    // await the array of promises with Promise.all
    const [one, two, three] = await Promise.all(promises);
    console.log(one);
    console.log(two);
    console.log(three);
    console.log(‘Done’);
    }
    test();
    // console shows ‘Now’ immediately
    // after one second, console shows 1, 2, 3, and ‘Done’

Async series parallel and waterfall

  • async.series: waiting for each preceding function to finish before starting next

  • async.parallel: launch them all functions simultaneously

  • async.waterfall: like series, but passed using an array

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    async.parallel({
    func1: function(cb) {
    cb(null, { foo: 'bar' });
    },
    func2: function(cb) {
    async.waterfall([
    function(cb2) {
    cb2(null, 'a');
    },
    function(prev, cb2) {
    cb2(null, 'b');
    }
    ], function(err, result) {
    cb(err, result);
    });
    }
    }, function(err, results) {
    callback(err, results);
    });
0%