Jest

Featured image

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.

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

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!