Making custom Jest assertion matchers in JavaScript and TypeScript
Tags: typescript, codenewbie, javascript, testing
Published: 2020-12-29 18:12:32

---
title: Making custom Jest assertion matchers in JavaScript and TypeScript
published: true
description:
tags: #typescript #codenewbie #javascript #testing
---

Jest is a powerful tool for testing your JavaScript code, giving an `expect` syntax that reads like a sentence, making it to reason about, such as:

```javascript
let someCuteAnimals = ['sloths', 'lemurs', 'gophers'];
expect(someCuteAnimals).toContain('sloths')
```

Each one of those `expect` methods starting with `to` is called a **matcher**, and there are a lot of them, like `toBe` for checking that two values are equal, `toBeGreaterThan` for checking that a number is greater than another number, and `toMatch` to check that a string matches a regular expression.

Something really cool about `expect` is that if none of the built-in matchers fit what you want to test, you can **add your own matcher** with `expect.extend`, so in this tutorial, we're gonna learn:

* ✏️ how to teach Jest a new matcher
* 💬 how to customize the matcher's error message
* 🦋 how to have TypeScript recognize the matcher

This tutorial assumes you have some familiarity with how to write a Jest test, as well as the basics of TypeScript, namely the concept of interface types. If you're not too familiar with TypeScript declaration files just yet though, that's all right, we'll be looking at that near the end of the tutorial.

## 🕰 Devising a matcher

Let's say we made a GPS app for sloths to tell them the best path to climb in order to get to some tasty cecropia leaves. 🦥🍃 

Three-toed sloths have a speed of about 0.15mph, so 792 feet per hour or about 13 feet per minute. So a function to give a sloth an ETA for their climb might look something like:

```javascript
function climbingETA(startTime, distanceInFeet) {
  let durationInMin = distanceInFeet / 13;

  // convert to milliseconds, the smallest unit of duration that's
  // represented in a JavaScript Date.
  let durationInMS = Math.floor(durationInMin * 60 * 1000);

  return new Date(startTime.getTime() + durationInMS);
}
```

To test this, the things we would have our tests assert are things like that if a sloth starts climbing at a certain time, we get back an ETA that's a minute later for every 13 feet the sloth climbs, so that would look something like this pseudocode:

```javascript
test('it takes ten minutes to climb 130 feet', () => {
  let eta = climbingETA(threeOClock, 130);
  expect(eta).toBe(threeTen);
});
```

But while that works for round numbers like climbing 130 feet in 10 minutes, what if a sloth was climbing 131 feet? That's still basically ten minutes, but using the `toBe` matcher, we'd be expecting the ETA `toBe` some timeframe right down to millisecond precision. Writing that JavaScript `Date` would be painful to write and makes our tests cumbersome to read. So what if instead, we had the matcher `toBeWithinOneMinuteOf`? Then our test could look like this:

```javascript
test('it takes about ten minutes to climb 131 feet', () => {
  let eta = climbingETA(threeOClock, 130);
  expect(eta).toBeWithinOneMinuteOf(threeTen);
});
```

Now the code reads "expect the ETA for climbing 131 feet to be within a minute of 3:10 PM", not the over the top precision like "expect the ETA to be 3:10:04 and 615 milliseconds". Much less a headache to work with that test! So let's see how we can add our own custom matcher!

## ✏️ Teaching Jest a new matcher

First, let's start off by making our test file. If you're following along in your own code, in a new folder, add the file `gps.test.js` with this code:

```javascript
// in a real app this wouldn't be in the test coverage, but we'll
// keep it there to keep this tutorial's code simple
function climbingETA(startTime, distanceInFeet) {
  let durationInMin = distanceInFeet / 13;
  let durationInMS = Math.floor(durationInMin * 60 * 1000);
  return new Date(startTime.getTime() + durationInMS);
}

test('it takes about ten minutes to climb 131 feet', () => {
  // [TODO] Write the test coverage
});
```

Then, since we're using Jest, add Jest to our dependencies with:

```
yarn add --dev jest
```

Great, now we're all set up! For adding a new matcher, we use the [expect.extend method](https://jestjs.io/docs/en/expect#expectextendmatchers). We pass in an object with each matcher function we want to add to `expect`. So adding our matcher function would look like this:

```javascript
expect.extend({
  toBeWithinOneMinuteOf(got, expected) {
    // [TODO] write the matcher
  }
});
```

and the function has to return a JavaScript object with at least these two fields:

* `pass`, which is true if the value we pass into `expect` causes the matcher to succeed
* and `message`, which is a function deriving the error message to if the matcher fails

So let's add this `toBeWithinOneMinuteOf` matcher function to `gps.test.js`:

```javascript
expect.extend({
  toBeWithinOneMinuteOf(got, expected) {
    const oneMinute = 60 * 1000; // a minute in milliseconds

    let timeDiff = Math.abs(expected.getTime() - got.getTime());
    let timeDiffInSeconds = timeDiff / 1000;

    let pass = timeDiff < oneMinute;
    let message = () =>
      `${got} should be within a minute of ${expected}, ` +
        `actual difference: ${timeDiffInSeconds.toFixed(1)}s`;

    return { pass, message }
  }
});
```

We calculate the difference between the expected time and the actual time. If it's less than a minute, then in the object we return the `pass` field is true, causing the matcher to succeed. Otherwise, `pass` is false causing the matcher to fail.

In the object we return, if the test fails, Jest shows our error message specified with `message`. We had it tell us the actual difference, in seconds, between the time we expected and the time we got.

`expect()` now has a brand new method called `toBeWithinOneMinuteOf` it didn't have before, so let's try it out! Update our test to this code:

```javascript
test('it takes about ten minutes to climb 131 feet', () => {
  let threeOClock = new Date('2020-12-29T03:00:00');
  let threeTen    = new Date('2020-12-29T03:10:00');

  let eta = climbingETA(threeOClock, 131);
  expect(eta).toBeWithinOneMinuteOf(threeTen);
});
```

Then run `npx jest` and you should see not only does our new matcher work, but the test passed with flying colors! 🐦🌈

## 💬 Customizing the error message

The test passes, but let's see what happens if it were to fail. Let's change the expected time to 3:12 PM and see what error message we get:

```javascript
test('it takes about ten minutes to climb 131 feet', () => {
  let threeOClock = new Date('2020-12-29T03:00:00');
  let threeTen    = new Date('2020-12-29T03:10:00');
  let threeTwelve = new Date('2020-12-29T03:12:00');

  let eta = climbingETA(threeOClock, 131);
  expect(eta).toBeWithinOneMinuteOf(threeTwelve);
});
```

Run `npx jest` again, and the error message we get would look like this:

![Jest running in the command line with the error message saying "Tue Dec 29 2020 03:10:04 GMT-0500 (Eastern Standard Time) should be within a minute of Tue Dec 29 2020 03:12:00 GMT-0500 (Eastern Standard Time), actual difference: 115.4s"](https://dev-to-uploads.s3.amazonaws.com/i/l13u7fyyhj1ukh57tzna.png)

We get an accurate error message, but the timestamps for the actual and expected times are cumbersome to read. For times where we just want to know if they're a minute apart, we shouldn't need to think about the date and time zone, so let's simplify the error message function. If you're following along in your own editor, try changing the error message function to this code:

```javascript
let message = () => {
  let exp = expected.toLocaleTimeString();
  let gt = got.toLocaleTimeString();
  return `${gt} should be within a minute of ${exp}, ` +
    `actual difference: ${timeDiffInSeconds.toFixed(1)}s`;
}
```

`toLocaleTimeString` represents a JavaScript `Date` with just the hour, minute, and second of the timestamp, without time zone or date. So if we run the test again, the error message should be:

![The failing test, this time with the more legible error message "3:10:04 AM should be within a minute of 3:12:00 AM, actual difference: 115.4s"](https://dev-to-uploads.s3.amazonaws.com/i/7r80vz6o081nzdn7x7ku.png)

Much better! There's just one other problem. You can modify any Jest matcher with `not`, so what error message would we get if we changed our `expect` line to this?

```javascript
expect(eta).not.toBeWithinOneMinuteOf(threeTen);
```

Now the error message in the command line will look like this.

![Jest running with the failing test again, but now the error message says "3:10:04 AM should be within a minute of 3:10:00 AM"](https://dev-to-uploads.s3.amazonaws.com/i/mq96d4bd3dn1d8po1y6f.png)

We're saying that the time we got **should** be within a minute of the time we expected, but the test actually expects that the time we got is **not** within a minute, making a confusing error message.

The problem is, we're displaying the same error message whether `pass` is true or not. And a matcher with the `not` modifier fails when `pass` is true.

So that means when `pass` is true, the error message should say that the time we got **should not** be within a minute of the time we expected. Let's tweak the message one more time:

```javascript
let message = () => {
  let exp = expected.toLocaleTimeString();
  let gt = got.toLocaleTimeString();

  if (pass) {
    // error message when we have the not modifier, so pass is
    // supposed to be false
    return `${gt} should not be within a minute of ${exp}, ` +
      `difference: ${timeDiffInSeconds.toFixed(1)}s`;
  }
  // error message when we don't have the not modifier, so pass
  // is supposed to be true
  return `${gt} should be within a minute of ${exp}, ` +
    `actual difference: ${timeDiffInSeconds.toFixed(1)}s`;
}
```

Now if we run the test one more time with `npx jest`, we will get an error message that makes sense both with and without the `not` modifier! 🎉

If you're following along in your own code, remove the `not` modifier so the expect reads

```javascript
expect(eta).toBeWithinOneMinuteOf(threeTen);
```

and then let's see how we would use our matcher in TypeScript!

## 🦋 Running the test in TypeScript

Now let's see how we would get our new matcher to work in TypeScript. First, rename `gps.test.js` to `gps.test.ts`.

Now since we're doing TypeScript, we want to have a step of our testing where we check that everything's the right type before we go ahead and run the test. And there's a convenient preset for Jest for that called [ts-jest](https://kulshekhar.github.io/ts-jest/). Let's get ts-jest and TypeScript by running:

```
yarn add --dev typescript ts-jest
```

We install the dependencies, and if you look in the `node_modules/@types` folder, you'll see there's a `jest` package, because `@types/jest` ia a dependency of ts-jest. What that means for us is that the TypeScript compiler now knows about all TypeScript types for Jest, like the type of the `expect` function and all its matchers like `toBe`. This because by default, the TypeScript compiler [looks for type definitions in `node_modules/@types`](https://www.typescriptlang.org/tsconfig#types). We didn't have to install `@types/jest` ourselves!

To have Jest use `ts-jest`, we need to add just a bit of configuration. Add a new file named `jest.config.js` with this code:

```javascript
module.exports = {
  preset: 'ts-jest',
}
```

and now, ts-jest will run each time we run Jest, so let's try that out. Run `npx jest` and you'll get:

![Our tests running in the command line with the error message "Property 'toBeWithinOneMinuteOf' does not exist on type 'JestMatchersShape<Matchers<void, Date>, Matchers<Promise<void>, Date>>'"](https://dev-to-uploads.s3.amazonaws.com/i/74ja4b6thkeaqbedfcjp.png)

Another error message! This one is a type error from the TypeScript compiler, so let's take a closer look.

The type callers `Matchers` is the type of the object we get from the function `expect()`. When we do `expect(eta)`, the return value is a `Matchers` and it includes all the different built-in matcher methods on it like `toBe` and `toContain`.

When we ran `expect.extend`, though, in JavaScript, we gave that `Matchers` type a new `toBeWithinOneMinuteOf` method. However, the problem is, while JavaScript knows about that method, TypeScript doesn't.

If you're a deep-diver like me and want to see exactly where TypeScript gets the information on what the Matchers type looks like, it's under the TypeScript [Matchers interface](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/3f36e32cd8cbcba20c4f3bef92121f994e4df30c/types/jest/index.d.ts#L714). That interface has all the built-in matchers methods you can see in Jest's documentation, but not the one we made.

Luckily, you can tell the TypeScript compiler "the Jest Matchers interface includes all the matchers in `@types/jest`, but then it's also got these other matcher methods I wrote". We do this using a technique called [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html).

Basically, you make a **declaration file** like the `index.d.ts` file in `@types/jest`, with a `Matchers` interface that has just the methods you wrote. Then, TypeScript looks at the `Matchers` interface in your declaration file, plus the one in `@types/jest`, to get a combined definition of the Matchers that includes your methods.

To make the declaration, add this code to a file titled `jest.d.ts`.

```typescript
declare global {
  namespace jest {
    interface Matchers<R> {
      toBeWithinOneMinuteOf(expected: Date): R
    }
  }
}

export {};
```

* The line `namespace jest` indicates that we're declaring code in Jest's namespace.
* Under the Jest namespace, we are declaring code in `interface Matchers<R>`, which means we're defining properties and methods on the Jest `Matchers` interface type.
* Under that interface, we add our method `toBeWithinOneMinuteOf` and have it take in a `Date`, and return a generic type `R`.

With this defined, now run `npx jest` and TypeScript now knows about the `toBeWithinOneMinuteOf` method! 🎊 

![Jest suite running in the command line, with the test passing](https://dev-to-uploads.s3.amazonaws.com/i/1eshuccq5t1ec64j5gm6.png)

## 🗺 Where do we go next with this?

We've defined our own custom matcher, designed its error message, and by adding it to a `.d.ts` file, now TypeScript can work with the new method! Since we can do that, that means we can add custom matchers for pretty much any common pattern we want to test in our codebase.

In addition to custom matchers you wrote, the Jest community also has made a bunch of extra convenient matchers in a JS module `jest-extended`. You can check it out [here](https://github.com/jest-community/jest-extended), and its README file has some great documentation on each of its matchers!

When you're building a JavaScript app, as it grows, be on the lookout for places where it's often cumbersome to write test coverage with existing Jest Matchers. That might just be the opportunity to make a matcher that makes tests a whole lot easier for you and anyone else on your dev team to be able to write and reason about!