9 min to read
Jest
Migrando de Jasmine a Jest
Buenas! como andan? En este post quiero comentarles algunos conceptos, tips y compartirles lo aprendido durante la experiencia de migración de nuestros unit tests armados en Jasmine, hacia Jest. Logrando pasar de una velocidad de 1:49 min a 17 segs, para una misma librería.
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);
Para mockear servicios y/o módulos manualmente, creamos una carpeta con nombre: __mocks__
(case-sensitive), y los archivos o stubs de nuestros servicios, de manera tal que sean siblings de los reales.
Esto nos sirve para que esos mocks se compartan entre varios test suites.
Global jest mocks file
Este archivo es el importado en nuestro jest.config.js: import '../src/lib/testing/jest-global-mocks';
Podemos mockear y reutilizar estos mocks de dependencias e incluso otras libs en las test suites de nuestra lib:
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()
};
}
}));
En el tercer caso, mockeamos no sólo la dependencia de esa lib, sino que además, mockeamos clases que esa lib brinda. Como también métodos pertenecientes a esas clases.
En caso de que tengamos una test suite donde no queremos ese auto mockeo global, podemos desmockearla con la siguiente línea:
jest.unmock('@myapp/second-lib');
jasmine.createSpyObj
Ejemplo en Jasmine:
const serviceMock =
jasmine.createSpyObj('service', [ 'getSth', 'getSthElse' ]);
Reemplazamos por su equivalente en Jest:
const serviceMock = { getSth: jest.fn(), getSthElse: jest.fn() };
Recordando que en nuestra colección de providers, tenemos:
providers: [{ provide: service, useValue: serviceMock }]
Spies
spyOn(obj, 'method')
podríamos reemplazarlo por:
jest.spyOn(obj, 'method')
PERO: Tener en cuenta que con Jasmine, cuando creamos un spy, o agregamos .and.stub() no llamamos a los métodos reales. Por el contrario en Jest, si creamos el spy pero no llamamos a los métodos de mock, se estarán invocando los métodos reales. En el ejemplo de arriba, el caso de Jest estaría llamando al método real.
Por lo cual, el reemplazo anterior para que efectivamente no se invoque el método real quedaría así:
jest.spyOn(obj, 'method').mockImplementation();
Retornando strings:
spyOn(obj, 'method').and.returnValue('text');
por:
jest.spyOn(obj, 'method').mockReturnValue('text');
Retornando objetos:
spyOn(service, 'method').and.returnValue({
id: 123,
description: 'hello...',
});
por:
jest.spyOn(service, 'method').mockReturnValue({
id: 123,
description: 'hello...',
});
Spies del Router
spyOn(Router.prototype, 'navigate');
por:
jest.spyOn(Router.prototype, 'navigate').mockImplementation();
Spies de localStorage
const localStorageSpy = spyOn(localStorage, 'removeItem').and.stub();
por:
const localStorageSpy = jest.spyOn(Storage.prototype, 'removeItem').mockImplementation();
Spy de indexedDB
const indexDbSpy = spyOn(indexedDB, 'deleteDatabase').and.stub();
por:
const indexDbSpy = jest.spyOn(indexedDB, 'deleteDatabase').mockImplementation();
Nota: Para este caso particular, en nuestro test.ts estamos importando “fake-indexeddb/auto”; habiendo instalado previamente “fake-indexeddb”: “^3.1.7”, Detalles aquí.
jasmine.clock()
Si tenes tests donde mockeas fechas, podemos reemplazar:
jasmine.clock().mockDate(new Date(2020, 10, 10));
por:
const mockDate: any = new Date(2020, 10, 10);
const dateNowSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate)
Para restaurar la fecha, al final del test:
dateNowSpy.mockRestore();
o bien en el AfterAll:
afterAll(() => {
jest.useRealTimers();
});
Imports array en specs
Unexpected value 'undefined' imported by the module 'any_module'
En caso de ver el error de arriba, eliminar imports del beforeEach que son innecesarios resolverá el problema, los tests correrán sin depender de ellos.
VS Extension
Recomiendo Jest runner para correr test individuales y suites enteras desde interfaz.
Happy coding!