Jest

Featured image

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.

enter image description here

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

enter image description here

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

enter image description here

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!