anchnk
anchnk

Reputation: 301

Jest: restore original module implementation on a manual mock

I have a pretty common testing use case and I am not sure what's the best approach there.

Context

I would like to test a module that depends on a userland dependency. The userland dependency (neat-csv) exports a single function that returns a Promise.

Goal

I want to mock neat-csv's behavior so that it rejects with an error for one single test. Then I want to restore the original module implementation.

AFAIK, I can't use jest.spyOn here as the module exports a single function.

So I thought using manual mocks was appropriated and it works. However I can't figure it out how to restore the original implementation over a manual mock.

Simplified example

For simplicity here's a stripped down version of the module I am trying to test:

'use strict';

const neatCsv = require('neat-csv');

async function convertCsvToJson(apiResponse) {
  try {
    const result = await neatCsv(apiResponse.body, {
      separator: ';'
    });
    return result;
  } catch (parseError) {
    throw parseError;
  }
}

module.exports = {
  convertCsvToJson
};

And here's an attempt of testing that fails on the second test (non mocked version):

'use strict';

let neatCsv = require('neat-csv');
let { convertCsvToJson } = require('./module-under-test.js');

jest.mock('neat-csv', () =>
  jest.fn().mockRejectedValueOnce(new Error('Error while parsing'))
);

const csv = 'type;part\nunicorn;horn\nrainbow;pink';
const apiResponse = {
  body: csv
};

const rejectionOf = (promise) =>
  promise.then(
    (value) => {
      throw value;
    },
    (reason) => reason
  );

test('mocked version', async () => {
  const e = await rejectionOf(convertCsvToJson(apiResponse));
  expect(neatCsv).toHaveBeenCalledTimes(1);
  expect(e.message).toEqual('Error while parsing');
});

test('non mocked version', async () => {
  jest.resetModules();
  neatCsv = require('neat-csv');
  ({ convertCsvToJson } = require('./module-under-test.js'));

  const result = await convertCsvToJson(apiResponse);
  expect(JSON.stringify(result)).toEqual(
    '[{"type":"unicorn","part":"horn"},{"type":"rainbow","part":"pink"}]'
  );
});

I am wondering if jest is designed to do such things or if I am going the wrong way and should inject neat-csv instead ?

What would be the idiomatic way of handling this ?

Upvotes: 1

Views: 5477

Answers (1)

Patrick Hund
Patrick Hund

Reputation: 20236

Yes, Jest is designed to do such things.

The API method you are looking for is jest.doMock. It provides a way of mocking modules without the implicit hoisting that happens with jest.mock, allowing you to mock in the scope of tests.

Here is a working example of your test code that shows this:

const csv = 'type;part\nunicorn;horn\nrainbow;pink';
const apiResponse = {
    body: csv
};

const rejectionOf = promise =>
    promise.then(value => {
        throw value;
    }, reason => reason);

test('mocked version', async () => {
    jest.doMock('neat-csv', () => jest.fn().mockRejectedValueOnce(new Error('Error while parsing')));
    const neatCsv = require('neat-csv');
    const { convertCsvToJson } = require('./module-under-test.js');
    const e = await rejectionOf(convertCsvToJson(apiResponse));
    expect(neatCsv).toHaveBeenCalledTimes(1);
    expect(e.message).toEqual('Error while parsing');
    jest.restoreAllMocks();
});

test('non mocked version', async () => {
    const { convertCsvToJson } = require('./module-under-test.js');

    const result = await convertCsvToJson(apiResponse);
    expect(JSON.stringify(result)).toEqual('[{"type":"unicorn","part":"horn"},{"type":"rainbow","part":"pink"}]');
});

Upvotes: 4

Related Questions