2 Comments

Mocking TypeScript Modules with Sinon

Lately, my team has been looking for better ways to create and maintain mocks in our TypeScript project. In particular, we wanted an easy way to mock out modules that we built using Sinon.JS.

We had a few goals for our mocks:

  1. Specific: Each test should be able to specify the mocked module’s behavior to test edge cases.
  2. Concise: Each test should only mock the functions that it cares about.
  3. Accurate: The return type of each mocked function should match the actual return type.
  4. Maintainable: Adding a new function to a module should create minimal rework in existing tests.

To accomplish these goals, we created this function:


export function mockModule<T extends { [K: string]: any }>(moduleToMock: T, defaultMockValuesForMock: Partial<{ [K in keyof T]: T[K] }>) {
  return (sandbox: sinon.SinonSandbox, returnOverrides?: Partial<{ [K in keyof T]: T[K] }>): void => {
    const functions = Object.keys(moduleToMock);
    const returns = returnOverrides || {};
    functions.forEach((f) => {
      sandbox.stub(moduleToMock, f).callsFake(returns[f] || defaultMockValuesForMock[f]);
    });
  };
}

The function takes in a module and an object that defines the mocked behavior of each function. When invoked, mockModule returns a new function that takes two parameters: a Sinon Sandbox instance and an object that can override the mocked values specified in the previous function.

Here’s an example of how mockModule can be used:


import * as sinon from 'sinon';
import { mockModule } from 'test/helpers';
import * as UserRepository from 'repository/user-repository';
import { getFullName } from 'util/user-helpers';

describe('getFullName', () => {
  const mockUserRepository = mockModule(UserRepository, {
    getFirstName: () => 'Joe',
    getLastName: () => 'Smith',
  });

  let sandbox: sinon.SinonSandbox;

  beforeEach(() => {
    sandbox = sinon.sandbox.create();
  });

  afterEach(() => {
    sandbox.restore();
  });

  it('returns the full name of a user', () => {
    mockUserRepository(sandbox);
    const fullName = getFullName({ userId: 1 });
    expect(fullName).to.equal('Joe Smith');
  });

  it('returns the full name of a user with only a first name', () => {
    mockUserRepository(sandbox, {
      getLastName: () => null,
    });
    const fullName = getFullName({ userId: 1 });
    expect(fullName).to.equal('Joe');
  });
});

This demonstrates my team’s general pattern for mocking modules. First, we use mockModule to create a function that can mock the given module. This happens at the outermost scope of our test suite so that the whole collection of tests can use the mocked function (in this example, the mockUserRepository function). Each test can call the mock function, and if needed, each test can specify new behaviors for the functions.

What we’ve found to be extremely helpful is the typing that mockModule provides. If we change the return type of a function in a module, we’ll receive a type error letting us know that we should update our tests accordingly.

This function has helped my team create better tests that are easy to write and maintain. What other mocking practices has your team used? Let me know in the comments.