JavaScript Testing: Jasmine to Jest

What you need to know when switching to Jest

JavaScript Testing: Jasmine to Jest

posted in javascript on

Jasmine vs Jest

When you search “Jasmine vs Jest”, you get a lot of high level comparisons which might be helpful when you have yet to write your first test. This is no such article, this is the article that answers the question

What are the actual syntax and API differences between Jasmine and Jest?

Head to the bottom of the article for the tl;dr difference table or for the slick VSCode integration.


Jest does seem to have more ⭐⭐⭐

jasmine/jasmine : Simple JavaScript testing framework for browsers and node.js

facebook/jest : Delightful JavaScript Testing.

Automatic Migration

skovhus/jest-codemods : Codemods for migrating to Jest

Instead of going through the entire Jest documentation to find out what the differences are between Jasmine and Jest, I just used jest-codemods followed by a git diff.

I can be so efficient sometimes.

npx jest-codemods --help
npx jest-codemods .

Configuration

Create a jest.config.ts:

npx jest --init

In Jasmine there was spec/helpers, now the entry point for global configuration is in jest.config.ts.

// A list of paths to modules that run some code to configure
// or set up the testing framework before each test
setupFilesAfterEnv: ["./setupJest.js"],

Example setupJest.js:

// Set a global variable
global.__DEV__ = true;

// Or a global function
global.t = t => t;

// Custom equality tester
// Return undefined if you want to give other testers a chance
const reInventToBe = (a, b) => a === b;
expect.addEqualityTesters([reInventToBe]);

// Custom matchers
expect.extend({ yourMatcher });

Custom Matchers

Use expect.extend instead of jasmine.addMatchers.

The return type of a custom matcher is slightly different in Jest: The message type changes from string to () => string.

expect.extend({
  yourMatcher(actual, expected, y, z) {
    return {
      pass: true,
      message: () => {
        const prettyActual = this.utils.printReceived(actual);
        const prettyExpected = this.utils.printExpected(expected);
        return `${prettyActual} is not something by ${prettyExpected}`;
      },
    };
  },
});

// Usage
expect(100).yourMatcher(99, 'param-y', 'param-z');

The Differences

  • In Jest test is an alias for it.
  • jasmine.xxx matchers typically translate directly to expect.xxx.
    • Ex: jasmine.arrayContaining –> expect.arrayContaining.
    • Jasmine has some (pretty useless) without equivalent in Jest.
    • But Jest has bunches more than Jasmine…
  • Jest has test.each to run an array of inputs

Spies

There are some differences in syntax when using spies, most is solved automatically by the code-mod. Jest offers a cleaner API with additional functionality.

But a big difference here is that while Jasmine does not execute the original implementation, Jest DOES.

// Setting up the Spy
const mataHari = {
  setBar(value, times) {return value * times;}
};

// The same behavior in Jasmine: spyOn(x, y).and.callThrough()
const spie = jest.spyOn(mataHari, 'setBar');


// If you want to mimick Jasmine behavior of NOT calling the original impl:
// In this case it doesn't matter much because there are no side effects
// If you are mocking an API call, you may want the Jasmine behavior instead!
// jest.spyOn(mataHari, 'setBar').mockImplementation(() => 0);


// Making calls
mataHari.setBar(1);
mataHari.setBar(2, 10);

// Inspecting calls
expect(mataHari.setBar.mock.calls[0][0]).toBe(1);
expect(mataHari.setBar.mock.calls[1][0]).toBe(2);
expect(mataHari.setBar.mock.calls[1][1]).toBe(10);
expect(mataHari.setBar.mock.lastCall[1]).toBe(10);

// Inspecting call returns
expect(mataHari.setBar.mock.results).toHaveLength(2);
expect(mataHari.setBar.mock.results[1].type).toBe('return'); // or 'throw'
expect(mataHari.setBar.mock.results[1].value).toBe(20); // == 2 * 10

// Clearing calls, results, ...
expect(mataHari.setBar.mock.calls).toHaveLength(2);
spie.mockClear();
expect(mataHari.setBar.mock.calls).toHaveLength(0);

// Also remove any mockReturnValue etc
spie.mockReset();

// In an afterEach or something
jest.restoreAllMocks();

proxyquire –> Mocks

Replace the api.js dependency in cut.js:

const apiStub = {};

// proxyquire
const proxyquire = require('proxyquire');
const cut = proxyquire('./cut.js', {'./api.js': apiStub});

// Jest
jest.mock('./api.js', () => apiStub);
const cut = require('./cut.js');

So the proxyquire package is not needed in Jest. Jest mocks are also much more versatile…

Async

Jasmine has done.fail(err). This has been simplified to just done(err) in Jest.

const pie = Promise.resolve(3.14);

// Jasmine
await expectAsync(pie).toBeResolvedTo(3.14);

// Jest
return expect(pie).resolves.toBe(3.14);

// Jest: Setting up mock Promises
const p1 = jest.fn().mockResolvedValue(1);
const p2 = jest.fn().mockRejectedValue(err);
// is shorthand for
const p1 = jest.fn().mockImplementation(() => Promise.resolve(1));

TL;DR - Differences Table

The tl;dr version:

Jasmine Jest Remarks
Matchers    
toMatch(‘\\sbar’) toMatch(/\sbar/)  
toContain(partialObj) toContainEqual(partialObj) Or toMatchObject()
withContext Extra describe()  
toThrowMatching toThrowError(/regex/)  
     
Spies    
jasmine.createSpy(‘n’) jest.fn() + mockName(‘n’) and getMockName()
jasmine.createSpyObj(‘n’, [‘f1’, ‘f2’]) ({ f1: jest.fn(), f2: jest.fn() }) With f1, f2 being the functions
jasmine.createSpyObj(‘n’, {f: 1}) ({ f: jest.fn(() => 1)})  
spyOn jest.spyOn spyOn is a global in Jasmine
spyOnProperty(obj, ‘prop’, ‘get’) jest.spyOn(obj, ‘prop’, ‘get’) With ‘prop’ the property name and the third parameter ‘get’ or ‘set’
.calls .mock.calls Ex: obj.prop.mock.calls
.calls.any() / .count() .mock.calls.length  
.calls.mostRecent .mock.lastCall  
.calls.reset() .mockClear() In Jest this is on the mock object itself, not on .calls
.and.callFake(fn) .mockImplementation(fn)  
.and.returnValue(1) .mockReturnValue(2) Ex: jest.spyOn(obj, ‘prop’).mockReturnValue(2);
.and.returnValues(1, 2) .mockReturnValueOnce(1) .mockReturnValueOnce(2)  
.and.throwError(err) .mockImplementation(() => {throw err})  
     
Async    
expectAsync resolves/rejects Ex: expect(promise).resolves.toBe(true)
toBeResolvedTo .resolves.toBe  
toBeRejectedWith .rejects.toMatch  
     
Time Travel   Manipulate new Date()
jasmine.clock().install() jest.useFakeTimers()  
jasmine.clock().tick(1) jest.advanceTimersByTime(1) Also: advanceTimersByTimeAsync
jasmine.clock().mockDate(d) jest.setSystemTime(d)  
jasmine.clock().uninstall() jest.useRealTimers()  
??? jest.runAllTimersAsync() Also: runOnlyPendingTimers, advanceTimersToNextTimer, …
Other    
jasmine.addMatchers expect.extend  
addCustomEqualityTester(t) addEqualityTesters([t]) Jasmine: 1 tester, Jest: Array of tester(s)!
proxyquire jest.mock  

VSCode Integration

Jest works out of the box when using create-react-app! (link contains the VSCode launch.json) after installing the Jest VSCode Extension:

jest-community/vscode-jest : The optimal flow for Jest based testing in VS Code

How it looks:

Visual UnitTest result cues in Visual Studio Code

Notifications?

Set notify: true in jest.config.ts.

npm install -S node-notifier

And get that dopamine shot every time it says 0 tests failed 😃

Conclusions

Key points:

  • If you have an existing code base, use jest-codemods.
  • The matcher syntax is almost 100% identical.
  • Timers syntax jasmine.clock vs jest.useFakeTimers
    • It’s a completely different API
    • But it’s converted 100% by the code-mod
    • Jest offers more control over timers
  • Spies: A slightly different API, also code-modded for you!
  • proxyquire is built in with jest.mock

Next up, the real reason to switch to Jest: React Component testing.

Well aside of, you know, the very nice VSCode integration which works out of the box with create-react-app.

Also note that today is truly the perfect day to publish a “Jest” article!


Stuff that came into being during the making of this post
Other interesting reads
Tags: tutorial testing