Writing Custom Matchers

Learn how to write and implement custom matchers.

Our applications are nuanced and will have nuanced testing needs. If Jest and other matcher libraries don’t suffice, we have the option to actually create our own matchers.

When to write a custom matcher

The tools that we already have will get us pretty far. Native matchers should be the first thing we reach for in tests. They are high quality, reliable, easily readable, and versatile.

Custom matchers should be reserved for specific logic that isn’t easily testable with these existing tools. Ultimately, a custom matcher will still execute in the same way. It will return true or false, and it will pass or fail based on whether or not something happened.

How to write a custom matcher

A custom matcher needs to be written as a function with a specific structure, which is then extended from expect (the same way matching libraries are integrated into our testing environment).

The function should accept at least two arguments. The first will represent whatever it is that we are making an assertion about. This is ultimately passed to expect. The second (and any other subsequent arguments) will be passed to the matcher.

Ultimately, our custom matcher should return an object with two fields:

  • pass: A boolean representing whether the test passed or failed.
  • message: An anonymous function returning a string message to be displayed if the test does’t pass.

This would look something like the following:

customMatcher(received, expected) {
  return {
    pass: true,
    message: () => '',
  };
};

The idea here is that inside this function, we can perform logic that conditionally returns the pass and message fields based on this logic.

Let’s pretend we have a User factory that autogenerates users for us. It contains two fields. The first is name, which is a string. The other is requireName, which is a boolean that we are particularly interested in because the validity of the first depends on the value of the second.

If requireName is false, '' is a valid value for name. However, if it’s true, then that value is invalid. We can create a custom matcher for this by adding this logic to our function, as illustrated below:

// new User() will generate { ... name: string, requireName: boolean };

toEnforceNameRequirements(receivedUser) {
  if (receivedUser.requireName && name.length === 0) {
    return {
      pass: false,
      message: () => 'name is empty and required',
    };
  };

  return {
    pass: true,
    message: () => '',
  };
}

To use the matcher above, we extend expect with it:

expect.extend({
  toEnforceNameRequirements(receivedUser) {
    if (receivedUser.requireName && receivedUser.name.length === 0) {
      return {
        pass: false,
        message: () => 'name is empty and required',
      };
    };

    return {
      pass: true,
      message: () => '',
    };
  },
});

We can now access it like any other matcher:

expect(new User()).toEnforceNameRequirements();

If our factory autogenerates a name that doesn’t meet our name requirements, the test will fail. Try it for yourself by running yarn test below:

Get hands-on with 1200+ tech skills courses.