4 min to read
Jest
Migrating from Jasmine to Jest
Howdy? In this post I want to share some concepts, tips and what I’ve learned during the migration of our unit tests built in Jasmine, to Jest. We managed to go from a speed of 1:49 minutes to 17 seconds, for the same library.
Stack
"jest": "26.2.2",
"jest-preset-angular": "8.3.2",
"@types/jest": "26.0.8",
"ts-jest": "26.4.0",
"@nrwl/jest": "11.2.0",
"@angular/core": "12.2.14",
"@nrwl/angular": "11.0.1",
Setup
Jest.config
module.exports = {
displayName: 'your_lib',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test.ts'],
globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: {
before: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer',
],
},
},
},
coverageDirectory: '../../coverage/libs/your_lib',
collectCoverage: true,
snapshotSerializers: [
'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js',
'jest-preset-angular/build/AngularSnapshotSerializer.js',
'jest-preset-angular/build/HTMLCommentSerializer.js',
],
setupFiles:["jest-canvas-mock"]
};
Angular.json
"test": {
"builder": "@nrwl/jest:jest",
"outputs": ["coverage/libs/your_lib"],
"options": {
"jestConfig": "libs/your_lib/jest.config.js",
"passWithNoTests": true,
"codeCoverage": true,
"collectCoverage": true,
"coverageReporters": ["json", "html"]
}
}
tsconfigs
"types": ["jest", "node"],
tsconfig.spec
"experimentalDecorators": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true
test.ts
import 'jest-preset-angular';
import '../src/lib/testing/jest-global-mocks';
import "fake-indexeddb/auto";
Cheat sheet: Jasmine to Jest
Mocks folder
Script from here
import * as fs from 'fs';
import * as path from 'path';
const isDirectory = (dir, file) =>
fs.statSync(path.join(dir, file)).isDirectory();
const mockExists = (dir, file) =>
fs.existsSync(path.join(dir, '__mocks__', file));
const mockModule = (dir, file) => {
jest.doMock(path.join(dir, file), () =>
jest.requireActual(path.join(dir, '__mocks__', file)));
};
const initMocks = (dir) => {
fs.readdirSync(dir)
.forEach((file) => {
if (isDirectory(dir, file)) {
initMocks(path.join(dir, file));
} else if (mockExists(dir, file)) {
mockModule(dir, file);
}
});
};
initMocks(__dirname);
To mock services and/or modules manually, we create a folder with name: __mocks__
(case-sensitive), and the files or stubs of our services, so that they are siblings of the real ones.
This allows us to share those mocks among several test suites.
Global jest mocks file
This file is the one imported in our jest.config.js: import '../src/lib/testing/jest-global-mocks';
.
We can mock and reuse these mocks from dependencies and even other libs in our lib test suites:
jest.mock('@myapp/lib');
jest.mock('@myapp/second-lib', () => jest.fn());
jest.mock('@myapp/third-lib', () => ({
'some-exported-class': jest.fn(),
'another-exported-class': jest.fn(),
'class-with-methods': () => {
return {
method: jest.fn(),
secondMethod: jest.fn()
};
}
}));
In the third case, we mock not only the dependency of that lib, but we also mock classes provided by that lib. As well as methods belonging to those classes.
In case we have a test suite where we do not want this global auto-mocking, we can unmock it with the following line:
jest.unmock('@myapp/second-lib');
jasmine.createSpyObj
Jasmine:
const serviceMock =
jasmine.createSpyObj('service', [ 'getSth', 'getSthElse' ]);
Jest:
const serviceMock = { getSth: jest.fn(), getSthElse: jest.fn() };
Remember our providers collection should have the following:
providers: [{ provide: service, useValue: serviceMock }]
Spies
from:
spyOn(obj, 'method')
to:
jest.spyOn(obj, 'method')
BUT: Keep in mind that with Jasmine, when we create a spy, or add .and.stub() we do not call the real methods. On the contrary in Jest, if we create the spy but we do not call the mock methods, the real methods will be invoked. In the example above, the Jest case would be calling the actual method.
Therefore, the above replacement so that the real method is not invoked would look like this:
jest.spyOn(obj, 'method').mockImplementation();
Returning strings from:
spyOn(obj, 'method').and.returnValue('text');
to:
jest.spyOn(obj, 'method').mockReturnValue('text');
Returning objects from:
spyOn(service, 'method').and.returnValue({
id: 123,
description: 'hello...',
});
to:
jest.spyOn(service, 'method').mockReturnValue({
id: 123,
description: 'hello...',
});
Router Spies
spyOn(Router.prototype, 'navigate');
to:
jest.spyOn(Router.prototype, 'navigate').mockImplementation();
localStorage Spies
const localStorageSpy = spyOn(localStorage, 'removeItem').and.stub();
to:
const localStorageSpy = jest.spyOn(Storage.prototype, 'removeItem').mockImplementation();
indexedDB
const indexDbSpy = spyOn(indexedDB, 'deleteDatabase').and.stub();
to:
const indexDbSpy = jest.spyOn(indexedDB, 'deleteDatabase').mockImplementation();
Note: For this particular case, in our test.ts we are importing “fake-indexeddb/auto”; having previously installed “fake-indexeddb”: “^3.1.7”, Details here.
jasmine.clock()
If you have tests where you mock dates, we can replace:
jasmine.clock().mockDate(new Date(2020, 10, 10));
to something like:
const mockDate: any = new Date(2020, 10, 10);
const dateNowSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate)
To restore it, at the end of the test:
dateNowSpy.mockRestore();
or:
afterAll(() => {
jest.useRealTimers();
});
Imports array in specs
Unexpected value 'undefined' imported by the module 'any_module'.
In case you see the above error, removing unnecessary imports from the beforeEach will solve the problem, the tests will run without relying on them.
VS Extension
I recommend Jest runner to run individual tests and entire suites from interface.
Happy coding!