Writing Custom Matchers
Learn how to write and implement custom matchers.
We'll cover the following
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:
Aboolean
representing whether the test passed or failed.message:
An anonymous function returning astring
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 1400+ tech skills courses.