Comparing jest.mock and Dependency Injection in TypeScript
Tags: typescript, javascript, webdev, node
Published: 2021-03-24 21:35:51

---
title: Comparing jest.mock and Dependency Injection in TypeScript
published: true
description: Comparing jest.mock and Dependency Injection in TypeScript
tags: typescript, javascript, webdev, node
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aipl77e8ly25fd055vj4.png
---

This post compares two strategies for mocking dependencies in your code for testing purposes. The example shown here focuses on a controller in Express, but the principles can be applied more widely.

A controller usually has some logic of it's own. In our simplified example, it needs to:

1. Validate the request payload
2. Call some business logic
3. Prepare the response payload
4. Respond

The controller code might look like this:

```ts
import { Request, Response } from "express";
import { CreatePersonReqBody, CreatePersonResBody } from "./api_contract";
import { createPerson } from "./domain";

export const createPersonAction = (
  req: Request<{}, CreatePersonResBody, CreatePersonReqBody>,
  res: Response<CreatePersonResBody>
) => {
  // Validate request payload
  if (!req.body.name) {
    res.status(400).json({ error: "name is required" });
    return;
  }

  try {
    // Call inner layer, which may be non-deterministic
    const person = createPerson({
      name: req.body.name,
      favouriteColour: req.body.favouriteColour,
    });

    // Build response payload
    const personPayload = { data: person, type: "person" } as const;

    // Respond
    res.json(personPayload);
  } catch (e) {
    res.status(400).json({ error: e.message });
  }
};
```

To test this code in isolation, we can mock the call to `createPerson`. That will allow us to focus solely on the responsibilities of this function. `createPerson` will have concerns of its own, and will likely hit a database or another API. Mocking the call to `createPerson` will keep our unit test running fast and predictably.

For the purposes of this example, we'd like to test two scenarios:

1. What does our controller do if `createPerson` throws an error?
2. What does our controller do in the happy path?

One option is to use `jest.mock` to fake the implementation of `createPerson`. Let's see what that looks like:

```ts
import { getMockReq, getMockRes } from "@jest-mock/express";
import { createPersonAction } from "./controller";
import { ICreatePersonData, IPerson, createPerson } from "./domain";

jest.mock("./domain", () => ({
  createPerson: jest
    .fn<IPerson, ICreatePersonData[]>()
    .mockImplementation((data) => ({ id: 1, name: data.name })),
}));

describe("controller", () => {
  beforeEach(() => jest.clearAllMocks());

  describe("createPerson", () => {
    it("responds with 400 if the colour is invalid", () => {
      (createPerson as jest.Mock).mockImplementationOnce(() => {
        throw new Error("Invalid Colour");
      });

      const req = getMockReq({
        body: { name: "Alan", favouriteColour: "rain" },
      });
      const { res } = getMockRes();

      createPersonAction(req, res);

      expect(createPerson).toHaveBeenCalledWith({
        name: "Alan",
        favouriteColour: "rain",
      });
      expect(res.status).toHaveBeenCalledWith(400);
      expect(res.json).toHaveBeenCalledWith({ error: "Invalid Colour" });
    });

    it("adds the type to the response payload", () => {
      const req = getMockReq({ body: { name: "Alice" } });
      const { res } = getMockRes();

      createPersonAction(req, res);

      expect(res.json).toHaveBeenCalledWith({
        data: { id: 1, name: "Alice" },
        type: "person",
      });
    });
  });
});
```

# Observations

## It's simple

`jest.mock` lets us choose the file we want to fake, and provide an implementation. Once the code is written it's clear to understand the intention.

## We're bypassing TypeScript

`jest.mock` has no knowledge of what it's mocking or what type constraints the implementation should adhere to. Similarly when we want to check if our spy was called, TypeScript doesn't know that this is a jest object. This is why we have to cast the function `as jest.Mock`.

## Shared state and mutation

The fake implementation defined at the top is shared across all tests in the file. That means that spied calls to the fake implementation are shared across tests. So if we want to spy on our fake implementation, and be sure that we're only dealing with calls from each individual test, we need to remember to `clearAllMocks` before every test.

Furthermore, when we want to override the fake behaviour for an individual test, we need to mutate the overall mock and remember to use `mockImplementationOnce` instead of `mockImplementation`. If we forget, the new implementation will be present for the next test.

## Strange behaviour with custom error classes!

I ran in to some odd behaviour when I tried to fake an implementation that threw an error from a custom error class. Perhaps this was human error on my part but I just couldn't figure it out. The error I'm getting is:

```ts
"domain_1.InvalidColourError is not a constructor"
```

I'm not sure what's going on here - if you know / have a solution please comment below! If you know of ways to overcome any of the other issues, also let me know!

As the title of this post suggests, there is an alternative approach to `jest.mock` - Dependency Injection. Dependency Injection is a fancy way of saying that we're going to pass in functions that we want to call in our application code (instead of hard coding them). This gives a first-class way of swapping out behaviour as desired.

To enable this in our test, instead of calling `jest.mock`, we're going to use a utility function that is so small that we can write it ourselves. Don't worry if you don't understand it and feel free to skip over it:

```ts
export const inject = <Dependencies, FunctionFactory>(
  buildFunction: (dependencies: Dependencies) => FunctionFactory,
  buildDependencies: () => Dependencies
) => (dependencies = buildDependencies()) => ({
  execute: buildFunction(dependencies),
  dependencies,
});
```

In short, it returns an object with an `execute` function that lets you call your controller action, and a `dependencies` object, which contains the mocks (useful when you want to spy on your calls).

To make use of this in our test, we need to make one small change to our controller:

```ts
import { Request, Response } from "express";
import { createPerson } from "./domain";
import { CreatePersonReqBody, CreatePersonResBody } from "./api_contract";

export const buildCreatePersonAction = (dependencies = { createPerson }) => (
  req: Request<{}, CreatePersonResBody, CreatePersonReqBody>,
  res: Response<CreatePersonResBody>
) => {
  // Validate request payload
  if (!req.body.name) {
    res.status(400).json({ error: "name is required" });
    return;
  }

  try {
    // Call inner layer, which may be non-deterministic
    const person = dependencies.createPerson({
      name: req.body.name,
      favouriteColour: req.body.favouriteColour,
    });

    // Build response payload
    const personPayload = { data: person, type: "person" } as const;

    // Respond
    res.json(personPayload);
  } catch (e) {
    res.status(400).json({ error: e.message });
  }
};
```

Did you spot the difference?

The only change here is that our exported function is a higher-order function, i.e. it's a function that returns another function. This allows us to optionally pass in our dependencies at runtime. If we don't pass anything in, we get the real production dependency by default. The function we get back is the express controller action, with any dependencies now baked in. Everything else is exactly the same.

Now for the test:

```ts
import { getMockReq, getMockRes } from "@jest-mock/express";
import { buildCreatePersonAction } from "./controller_w_di";
import { ICreatePersonData, IPerson, InvalidColourError } from "./domain";
import { inject } from "./test_utils";

const buildAction = inject(buildCreatePersonAction, () => ({
  createPerson: jest
    .fn<IPerson, ICreatePersonData[]>()
    .mockImplementation((data) => ({ id: 1, name: data.name })),
}));

describe("controller", () => {
  describe("createPerson", () => {
    it("responds with 400 if the colour is invalid", () => {
      const req = getMockReq({
        body: { name: "Alan", favouriteColour: "rain" },
      });
      const { res } = getMockRes();

      const { dependencies, execute } = buildAction({
        createPerson: jest
          .fn()
          .mockImplementation((data: ICreatePersonData) => {
            throw new InvalidColourError();
          }),
      });

      execute(req, res);

      expect(dependencies.createPerson).toHaveBeenCalledWith({
        name: "Alan",
        favouriteColour: "rain",
      });
      expect(res.status).toHaveBeenCalledWith(400);
      expect(res.json).toHaveBeenCalledWith({ error: "Invalid Colour" });
    });

    it("adds the type to the response payload", () => {
      const req = getMockReq({ body: { name: "Alice" } });
      const { res } = getMockRes();

      buildAction().execute(req, res);

      expect(res.json).toHaveBeenCalledWith({
        data: { id: 1, name: "Alice" },
        type: "person",
      });
    });
  });
});
```

# Observations

## jest.mock replaced by inject

As we mentioned, instead of `jest.mock` we have an `inject` function which wires up the fake dependency for us.

## No shared state or mutation

There's no need to clear any mocks, because we generate a new injected action each time. We can use `mockImplementation` or `mockImplementationOnce` as we please as the scope is limited to the test. Each test case has it's own fresh version of the controller action, it's dependencies and mocks. Nothing is shared.

## Fully type-safe

Because we're dealing with functions and arguments instead of overriding modules, everything is type checked. If I forgot to provide an `id` in my fake implementation, TypeScript will tell me.

## No custom error class issues

I didn't see the same issues with the custom error class that I saw with the `jest.mock` approach. It just worked. Again, perhaps this is human error. Please comment below if you know what's going on here.

## Less familiar pattern

Developers who are used to seeing `jest.mock` might be confused by the `inject` call. That said, the differences in usage compared to the `jest.mock` version are minimal. With this method we're passing a function and an implementation rather than a string (containing the module) and an implementation.

# Conclusion

Personally I think there are nice benefits to using the dependency injection style of mocking. If you're not using TypeScript, the benefits are less, but you still have the shared state aspects to worry about. I've seen it lead to strange test behaviour and flakiness in the past that can be hard to track down.

Dependency Injection is a useful pattern to be familiar with. When used in the right spots, it can help you write code that is loosely coupled and more testable. It's a classic pattern in software development, used in many languages, and so it's  worthwhile knowing when and how to use it.

A final shout out goes to the authors of `@jest-mock/express` - a very useful library that let's you stub your Express requests and responses in a type-safe way. Kudos!

The full code is available [here](https://github.com/keithbro/jest-mock-v-di).

# Update!

A third option exists: `jest.spyOn`!

With no need for the higher-order function in the controller, your test can look like:

```ts
import { getMockReq, getMockRes } from "@jest-mock/express";
import { createPersonAction } from "./controller";
import * as Domain from "./domain";

describe("controller", () => {
  describe("createPerson", () => {
    beforeEach(() => {
      jest.clearAllMocks();
      jest.spyOn(Domain, "createPerson").mockImplementation((data) => {
        return { id: 1, name: data.name };
      });
    });

    it("responds with 400 if the colour is invalid", async () => {
      jest.spyOn(Domain, "createPerson").mockImplementationOnce(() => {
        throw new Domain.InvalidColourError();
      });
      const req = getMockReq({
        body: { name: "Alan", favouriteColour: "rain" },
      });
      const { res } = getMockRes();

      createPersonAction(req, res);

      expect(Domain.createPerson).toHaveBeenCalledWith({
        name: "Alan",
        favouriteColour: "rain",
      });
      expect(res.status).toHaveBeenCalledWith(400);
      expect(res.json).toHaveBeenCalledWith({ error: "Invalid Colour" });
    });

    it("adds the type to the response payload", async () => {
      const req = getMockReq({ body: { name: "Alice" } });
      const { res } = getMockRes();

      createPersonAction(req, res);

      expect(res.json).toHaveBeenCalledWith({
        data: { id: 1, name: "Alice" },
        type: "person",
      });
    });
  });
});

```

# Observations

## It's simple

It's pretty clear what's going on. Familiar patterns.

## TypeScript is partially supported

We do get type support when specifying a fake implementation. But TypeScript doesn't know that `Domain.createPerson` is a mock object, so if we wanted to inspect the `calls` we'd have to do:

```ts
(Domain.createPerson as jest.Mock).mock.calls
```

We can get around this by storing the return value of `mockImplementation` but this becomes a little untidy if you're doing this in a `beforeEach`.

## State is shared

State is shared across tests so we still need to `clearAllMocks` in our `beforeEach`.

## No issue with custom error classes

The custom error class issue did not occur with this approach.

# Final Conclusion

In my opinion `jest.spyOn` is a better option than `jest.mock` but still not as complete a solution as dependency injection. I can live with the TypeScript issue as it's minor, but shared state and tests potentially clobbering each others' setup is a big no.