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:
- Specific: Each test should be able to specify the mocked module’s behavior to test edge cases.
- Concise: Each test should only mock the functions that it cares about.
- Accurate: The return type of each mocked function should match the actual return type.
- 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.
Hi
Try to use your code. But what is R? And where did it come from and I need to define the type T in mockModule….
Hi Olle,
I replaced “R” with “Object”. “R.keys” is a function from Ramda: https://ramdajs.com/docs/#_keys. This library is not necessary for this function so I took it out.
“T” is a generic type that was being displayed properly in the code snippet. It should be present now.
I updated the post to reflect both of these changes. Thanks!
Very useful – thanks Andy! :)
Glad to hear it helped, Dacre!
hello
can u share us the link of the project. I am facing certain issues