TypeScript Testing Tips - Mocking Functions with Jest

28 October 2020

In this, the second of a series of posts on practical applications of TypeScript, we continue on the topic of testing by looking at how three lines of TypeScript magic can really improve the readability and conciseness of creating mocks with Jest.

Jest is a popular testing framework that covers all aspects of testing including mocking, verifying expectations, parallel test execution and code coverage reports. It’s also light on configuration so there’s a lot to like.

But the name: Jest, and in particular that J, betrays a potential weakness. It was originally built for JavaScript, and while they’ve added TypeScript support since version 24 some things still feel a bit clunky out of the box. Below we look at one such example and show how we make things a bit cleaner.

Mocking Simple Functions

Let’s say the code your testing calls out to the following helper function in a file called Converters.ts:

export function sterlingToEuros(amountSterling: number): number {
  // Perform lookup, calculation or call to another service
  ...
  return amountEuros;
}

For unit testing, you’ll need a MockedFunction for sterlingToEuros, which allows you to:

  • Control how the function behaves during tests via methods like mockReturnValue and mockReturnValueOnce.
  • Verify how your code interacted with the mock using, for example, expect to verify expectations.

To create the MockedFunction you need to mock the module containing the function:

jest.mock("./path/to/file/Converters.ts");

Now during test execution sterlingToEuros is a Jest MockedFunction, but TypeScript doesn’t know this at compile-time, so you’re not getting the benefits of static-typing during development.

You can cast it to the correct type with:

const sterlingToEurosMock = sterlingToEuros as jest.MockedFunction<(amountSterling: number) => number>;

but this is a bit long-winded, error-prone, and could detract from what’s important in the test. Bear in mind, also, that many functions will have a more complex signature perhaps having multiple parameters, custom types, generics or async, and so the above approach could get really cumbersome.

TypeScript’s type inference allows us to clean this up if we add the following helper:

export function mockFunction<T extends (...args: any[]) => any>(fn: T): jest.MockedFunction<T> {
  return fn as jest.MockedFunction<T>;
}

It probably makes sense to add this in a JestHelpers.ts file, and future posts will show other useful helper functions that could live alongside it.

You can now use this helper in your tests as follows:

const sterlingToEurosMock = mockFunction(sterlingToEuros);

eliminating the need to include a lengthy function signature. Your mock will have the correct type and you can use it as expected:

it("should report value in Euros", () => {
  sterlingToEurosMock.mockReturnValue(50);
  
  expect(getCost()).toEqual(50);
  
  expect(sterlingToEurosMock).toHaveBeenCalledWith(45);
});

The compiler will ensure you don’t make any type errors, for example:

// sterlingToEuros can only be set up to return type number
sterlingToEurosMock.mockReturnValue("50");                // Error
sterlingToEurosMock.mockReturnValue(null);                // Error

// sterlingToEuros can only be called with a single argument of type number
expect(sterlingToEurosMock).toHaveBeenCalledWith();       // Error
expect(sterlingToEurosMock).toHaveBeenCalledWith(12, 34); // Error 

How it works

That covers the main takeaway of the post, but if you’re interested in how the helper works, then read on.

As mentioned, mockFunction simply casts a Jest MockedFunction to the correct type. Because it uses a generic type for the input parameter it has access to the type of function being mocked and it uses this in the return type and in the implementation for casting. Here it is again:

export function mockFunction<T extends (...args: any[]) => any>(fn: T): jest.MockedFunction<T> {
  return fn as jest.MockedFunction<T>;
}

Notice how we’ve restricted the types that can be passed in by stipulating:

T extends (...args: any[]) => any>(fn: T)

Breaking this down:

  • (fn: T): The parameter is of type T, where
  • T extends: The type T must be compatible with
  • (...args: any[]) => any: A function, where
    • …args: Rest parameters are used so the function can take any number of arguments
    • any[]: The input parameters can have any type
    • => any: The function can have any return type.

These restrictions on the input parameter prevent us from errors such as:

const sterlingToEurosMock = mockFunction("sterlingToEuros"); // Error
// "sterlingToEuros" is a string, not a function

const sterlingToEurosMock = mockFunction(sterlingToEuros()); // Error
// This uses the return value from sterlingToEuros() i.e. a number, which is not a function 

Giving us further type-safety and leveraging the strengths of TypeScript so developers are alerted to errors at the earliest possible moment.

We’ve now covered creating dummy objects and mocking functions with TypeScript. In future posts, we’ll continue to build on this to show how we mock classes, constant objects, and object getter and setters.

Article By
blog author

Eoin Mullan

Principal Engineer