DEV Community

Derek Askham
Derek Askham

Posted on

Jest Testing: Mocking modules and handling module state using Typescript and ES6

Jest is a popular testing framework that works with projects such as Babel, TypeScript, Node, React, Angular, and Vue. Like other testing frameworks, Jest offers the ability to mock and spy on javascript objects and modules with their respective properties and functions. In addition, it offers a handful of flexible ways to mock javascript modules depending on your test's demands.

Image description

When mocking modules, tests will either need to persist or clear the state of objects shared between them. Consider the following:

a. The meta data contained in spied objects within a module, such as the number of times a function or property was called/accessed or the arguments that were passed in, may no longer be relevant and should be cleared if assertions in separate tests are made against the same spied function.

b. A module relies on a mock configuration object whose flags are expected to be toggled to different values between tests. The state of that module should be reset to reflect any changes to those flags.

Generally, resetting/clearing/restoring modules will be done within the setup and teardown functions built into Jest. (e.g. beforeEach(), afterEach()). How you reset the modules within these functions however depends on the type of modules your project is configured to use.

CommonJS Modules

These modules are brought in using the require syntax and is fairly straight-forward to use for resetting modules.

// module.js

exports.a = 1
exports.add = function (a) {
  return exports.a += a;
}

//module.test.js

let myModule;

beforeEach(async () => {
  myModule = require("./useMyModule");
});

afterEach(() => {
  jest.resetModules();
});

test("adds 1 to counter", () => {
  expect(myModule.add(1)).toBe(2);
  expect(myModule.add(1)).toBe(3);
});

test("adds 2 to counter", () => {
  expect(myModule.add(2)).toBe(3);
  expect(myModule.add(2)).toBe(5);
});

Enter fullscreen mode Exit fullscreen mode

ES6 Modules

These modules (e.g. when using TypeScript) are brought in using the importsyntax. The process to reset modules here becomes more nuanced and verbose. Modules need to be imported into local variables which are then re-imported between tests using isolateModules(). Each test or suite of tests should then set another local variable instance to that re-imported module. Any changes in behavior for mocked functions/modules used by the re-imported module (e.g. configuration flags) should be set before setting that local instance variable.

// useMyModule.ts

let value = 1;

function add(number: number): number {
  value += number;
  return value;
}

export default { add };

//useMyModule.test.ts  

import * as useMyModuleImport from "./useMyModule";

let useMyModule: typeof useMyModuleImport.default;

beforeEach(async () => {
  jest.isolateModules(async () => {
    return import("./useMyModule").then((module) => {
      useMyModule = module.default;
    });
  });
});

afterEach(() => {
  jest.resetModules();
});

let myModuleInstance: typeof useMyModule;
test("adds 1 to counter", () => {
  // Any mocked functions/modules used by useMyModule should be set here before setting myModuleInstance to the re-imported module. E.g. configuration flags.
  myModuleInstance = useMyModule;
  expect(myModuleInstance.add(1)).toBe(2);
  expect(myModuleInstance.add(1)).toBe(3);
});

test("adds 2 to counter", () => {
  myModuleInstance = useMyModule;
  expect(myModuleInstance.add(2)).toBe(3);
  expect(myModuleInstance.add(2)).toBe(5);
});

Enter fullscreen mode Exit fullscreen mode

As can be seen, both examples allow module state to be cleared between tests, the ES6 modules just require a little more set up. Happy testing!

Examples above were inspired from this stackoverflow post.

Top comments (0)