adrianhesketh.com

Simplifying TypeScript code with Jest mocking

If you’ve ever thought that your TypeScript code is too complex due to dealing with dependencies, this post is for you.

I’m going to start with a scenario. I have three functions that I want to compose together. Let’s call them “a”, “b” and “c”.

My function should call all 3 of them and return an array of the return values like this:

const getAll = () => [a(), b(), c()]

Just to make things interesting, these functions are all in different modules that use different ways of exporting their functionality.

a uses a named export

export const a = (_param: string) => "A";

b uses a default export

export default (_param: string) => "B";

c exports a class

export class C {
  constructor() {
    this.value = "C";
  }
  value: string;
  getValue(_param: string) {
    return this.value;
  }
}

Composing the dependencies

We now need a way to use all 3 of these dependencies to create the return value.

In the examples here, we’re not doing anything except returning a single character, but in the real world, functions “a”, “b” and “c” might get data from a database, make an API call and write to a database, so we want to be able to replace their implementations with mocks during testing so that we don’t end up doing integration testing.

This means that we’ll need some mechanism for doing dependency injection - i.e. passing the required functionality into the thing that’s using the dependencies to create the return value.

There are a few ways to do this.

Using a class

A popular way to do this in the .NET and Java world is to use a class. TypeScript has classes too, so it’s easy to understand if you have more of your programming experience in those languages.

The class’s constructor takes in the required dependencies and sets them as fields in the class. If no dependencies are provided, the imports are used as the default implementation.

One thing to note is that some imports had to be renamed - a became externalA and the default export became externalB so that they didn’t clash with the field names on the class a and b. Coming up with new names for your imports can be suprisingly difficult.

The class import C could stay as it was because it’s an upper case C and so it doesn’t clash with the lowercase field name c in the class. It’s also a common pattern in the .NET and Java worlds to take in an interface instead of a specific class, so I’ve demonstrated that. That allows us to pass anything that matches the interface value, not just a type of C.

The only useful bit of this code is the getAll function, so it’s a lot of code for a one line function.

import { a as externalA } from "../namedexport";
import externalB from "../defaultexport";
import { C } from "../classexport";

export interface ValueGetter {
  getValue(param: string): string
}

export class Getter {
  constructor(a = externalA, b = externalB, c: ValueGetter = new C()) {
    this.a = a;
    this.b = b;
    this.c = c;
  }
  a: (param: string) => string;
  b: (param: string) => string;
  c: ValueGetter;
  getAll(param: string): Array<string> {
    return [this.a(param), this.b(param), this.c.getValue(param)];
  }
}

However, it is easy to test the class by passing through anonymous functions (or jest.fn()) to replace the function imports, and by creating an anonymous type to replace C:

describe("class", () => {
  it("has default behaviour", () => {
    // Arrange.
    const getter = new Getter()

    // Act.
    const values = getter.getAll("");

    // Assert.
    expect(values).toEqual(["A", "B", "C"]);
  });
  it("can be mocked using dependency injection", () => {
    // Arrange.
    const newA = () => "x";
    const newB = () => "y";
    const newC = {
      getValue: () => "z",
    };
    const getter = new Getter(newA, newB, newC)

    // Act.
    const values = getter.getAll("");

    // Assert.
    expect(values).toEqual(["x", "y", "z"]);
  });
});

Using a higher-order function

Let’s look at simplifying this by getting rid of the class and interface. Immediately, we can see it’s a lot smaller and it’s a lot easier to get at what the thing does.

I’ve created a function type stringGetter to avoid typing out the same thing for each parameter, but that does add a bit of needless complexity.

import { a as externalA } from "../namedexport";
import externalB from "../defaultexport";
import { C } from "../classexport";

type stringGetter = (param: string) => string;

export const createGetter = (
  a: stringGetter = externalA,
  b: stringGetter = externalB,
  c: stringGetter = (param: string) => {
    return new C().getValue(param);
  }
) => (param: string) => [a(param), b(param), c(param)];

Let’s take out that complexity too:

import { a as externalA } from "../namedexport";
import externalB from "../defaultexport";
import { C } from "../classexport";

export const createGetter = (
  a = externalA,
  b = externalB,
  c = (param: string) => {
    return new C().getValue(param);
  }
) => (param: string) => [a(param), b(param), c(param)];

The tests are just a touch simpler because we’re able to pass a function for c instead of a thing that looks like a class.

Ultimately, all we’ve done is migrated from having fields on our class to the function returned by createGetter being able to access the createGetter arguments (a, b and c) in the function body without having to use the this keyword.

describe("higher-order function", () => {
  it("has default behaviour", () => {
    // Arrange.
    const getAll = createGetter();

    // Act.
    const values = getAll("");

    // Assert.
    expect(values).toEqual(["A", "B", "C"]);
  });
  it("can be mocked using dependency injection", () => {
    // Arrange.
    const newA = () => "x";
    const newB = () => "y";
    const newC = () => "z";
    const getAll = createGetter(newA, newB, newC);

    // Act.
    const values = getAll("");

    // Assert.
    expect(values).toEqual(["x", "y", "z"]);
  });
});

None of the above

Here’s the simplest implementation, the sort of thing you’d write before you learned about unit testing and dependency injection.

import { a } from "../namedexport";
import b from "../defaultexport";
import { C } from "../classexport";

export const getAll = (param: string) => [
  a(param),
  b(param),
  new C().getValue(param),
];

But… one way to think about this code is that the dependencies are injected. They’re injected in the first 3 lines.

The import statements are the code that defines the dependencies, just at the module (file) level rather than the function or class level. They’re easy to overlook.

If you think about it that way, you might think of the other mechanisms as actually duplicating the definitions of the dependencies.

But how can we replace those dependencies for testing? Well, fairly easily with Jest. Here’s an example of a test suite that replaces the dependencies of the getAll function using the jest.mock function. There’s a shorter way to write the test that doesn’t create the mockA, mockB and mockC variables, but I like to have them around so that I can change the return values of the mocks in other tests.

The initial mock setup is done up on a per-test-suite (per file) basis, but you can change the return values easily enough with mockC.mockReturnValueOnce("Z") or trigger an error with mockC.mockRejectedValueOnce(new Error("unknown")).

This is an interesting trade-off. We get simpler production code and better runtime performance with this design, because there’s no extra variables or function calls. It looks like the testing has more boiler plate, but it’s really because I added a check to ensure that the functions were actually called.

import { getAll } from "./di_jest_code";

// Arrange.
const mockA = jest.fn(() => "x");
jest.mock("../namedexport", () => ({
  a: () => mockA(),
}));
const mockB = jest.fn(() => "y");
jest.mock("../defaultexport", () => ({
  default: () => mockB(),
}));
const mockC = jest.fn(() => "z");
jest.mock("../classexport", () => ({
  C: () => ({
    getValue: ()=> mockC(),
  }),
}));

describe("higher-order function", () => {
  it("can be mocked with jest", () => {
    // Act.
    const values = getAll("");

    // Assert.
    expect(values).toEqual(["x", "y", "z"]);
    expect(mockA).toHaveBeenCalled()
    expect(mockB).toHaveBeenCalled()
    expect(mockC).toHaveBeenCalled()
  });
});

There’s also a way to mock all uses of a module across all test suites - jest looks for the existence of __mocks__ directories containing files that match the name of the imported module. These mocks are automatically applied to all tests at runtime unless a particular test suite opts out.

When I first started to try out jest mocking, I found it quite frustrating until I put a good set of examples together of mocking the various types of module exports (named, default and class) in TypeScript. Just to help people Googling for the answer, the error messages I got were:

TypeError: classexport_1.default is not a constructor
TypeError: namedexport_1.a is not a function
Matcher error: received value must be a mock or spy function

Full example

I’ve put together an example repository over at https://github.com/a-h/ts-jest-mock that contains all of the code examples. You can run the unit tests with npx jest.

The various dependency injection styles are in the https://github.com/a-h/ts-jest-mock/tree/master/styleexamples directory

Summary

Simplicity is about having fewer components interacting. Having higher order functions and extra parameters that are only there to support unit testing presents a complexity cost.

We can stop duplicating the dependency injection by removing dependency injection from function signatures and class definitions. Instead, we can rely on the fact that the import statements at the top of the module (file) already define the dependencies, and replace them using jest’s mocking features.

This produces production code that isn’t littered with plumbing that’s only used for testing at the cost of having to spend some time to understand jest mocking. I think it’s a trade-off that’s worth considering.