Software testing is a crucial aspect of the software development process, ensuring that code functions as intended and meets the desired requirements. Jest, developed by Facebook, is currently the most popular JavaScript testing framework, according to the State of JS 2022 survey, being used by almost 70% of JavaScript developers. It works well for both frontend and backend projects and is compatible with TypeScript.
Jest has a range of powerful tools that allow developers to test values in various ways. However, sometimes specific assertions can be cumbersome, depending on the project’s business rules. To overcome this issue, Jest allows us to create custom matchers. In this article, I’ll show you how to make your first Jest custom matcher and some best practices to have in mind.
What is a Matcher in Jest?
Matchers in Jest are functions for asserting specific conditions in tests, ranging from simple checks like toBe
for exact equality to more complex ones such as toContain
and toThrow
. They cover various testing scenarios, including number comparisons and string pattern validation. This variety ensures detailed, accurate testing of code functionalities.
Jest’s documentation provides a full list of matchers for comprehensive understanding. Here are some examples of key Jest matchers:
// Exact equality check
expect(value).toBe(3); // Passes if value is exactly 3
// Checking for the value to be null
expect(value).toBeNull(); // Passes if value is null
// Checking for the value to be truthy (i.e., not false, 0, '', null, undefined, or NaN)
expect(value).toBeTruthy(); // Passes if value is truthy
// Checking for numbers to be greater than a certain value
expect(value).toBeGreaterThan(10); // Passes if value is greater than 10
// Checking strings against regular expressions
expect(value).toMatch(/abc/); // Passes if value matches the regex /abc/
// Checking if an array contains a specific item
expect(array).toContain(item); // Passes if array contains item
// Checking if a function throws an error
expect(() => { myFunction(); }).toThrow(Error); // Passes if myFunction throws an Error
Understanding Jest Symmetric Matchers
Symmetric matchers are great for doing exact checks in tests. They make sure that the data you’re testing matches exactly what you expect. These matchers are really useful for checking things like:
// Checks if a value is exactly the same as another
expect(value).toBe(3); // Passes if value is exactly 3
// Checks if a list has a specific item
expect(array).toContain(item); // Works if array has the item
// Makes sure a number is bigger than a certain value
expect(number).toBeGreaterThan(100); // Good if number is more than 100
// Checks if an object looks exactly how you expect
expect(myObject).toMatchObject({ key: 'value' }); // Passes if myObject has these properties
These matchers are perfect when you need to be sure about the exact details, like how much something is or what it contains.
What are Asymmetric Matchers in Jest
Asymmetric matchers, on the other hand, are more flexible and let you do partial checks. This is really handy when you only care about certain parts of the data. They don’t need everything to match perfectly:
// Checks if a list includes certain things
expect(myArray).toEqual(
expect.arrayContaining([1, 2])
); // Passes if myArray has 1 and 2
// Checks if a spy function was called with a parameter of a certain type, like a number
expect(mySpyFn).toHaveBeenCalledWith(expect.any(Number)); // Passes if parameter is a number
// Checks if there is a value, but it doesn't matter what it is (as long as it's not null or undefined)
expect(value).toEqual(expect.anything());
These matchers are great for times when you’re not worried about every little detail but want to make sure some things are there or right. They give you a way to test without being too strict about it, which is helpful in many situations.
The Challenges of Handling Complexity with Built-in Matchers

Let’s suppose you’ve just been hired as a new Node.js Backend Engineer at Uber. For your first task, you need to implement a method to calculate the GPS coordinates of an autonomous car. This involves processing information from the car’s sensors and requesting data from an external satellite API.
Due to business policies, the API service only performs external requests every 5 seconds, returning a cached value in the meantime. Consequently, you need to adjust the received coordinates with the information gathered from the motion sensors, performing some calculations. In the end, your function might look like this:
type GPSCoordinate = {
latitude: number;
longitude: number;
}
async function computeGPSCoordinate(motionInfo: Sensors.MotionInformation): GPSCoordinate {
const satelliteInfo = await APIService.requestInformationFromSatellite();
// Perform some complex internal calculations
const carPositions = estimateRealTimePositions(satelliteInfo, motionInfo);
return {
latitude: Number(carPositions[0].y),
longitude: Number(carPositions[0].x)
};
}
Since this function is crucial to the functioning of the autonomous car, you need to ensure that it returns valid GPS coordinates. You start testing with various combinations of satellite and motion sensor information. Your tests might look like this:
const inputs = [
{
satelliteInfo: {
// Satellite Information 1
},
motionInfo: {
// Motion Information 1
},
},
// ...
];
test.each(inputs)("should produce valid GPS coordinates", async (input) => {
const mockAPIRequest = jest.spyOn(APIService, 'requestInformationFromSatellite');
mockAPIRequest.mockResolvedValue(input.satelliteInfo);
const output = await computeGPSCoordinate(input.motionInfo);
// Check if latitude is within the valid range
expect(output.latitude).toBeGreaterThanOrEqual(-90);
expect(output.latitude).toBeLessThanOrEqual(90);
// Check if longitude is within the valid range
expect(output.longitude).toBeGreaterThanOrEqual(-180);
expect(output.longitude).toBeLessThanOrEqual(180);
});
Here, you use Jest’s built-in matchers like toBeGreaterThanOrEqual
and toBeLessThanOrEqual
to ensure that the latitude and longitude values are within their valid ranges, requiring four different assertions.
As more specific business rules are applied, such as ensuring that the GPS coordinate fits within a particular country’s territory, you should include more assertions. This can make the test less readable, especially as the complexity of the conditions increases.
Crafting a Jest Custom Matcher: Simplifying Complex Validations
In our last discussion, we saw how using built-in Jest matchers for complex tests can be quite a handful. Lots of checks, many lines, and let’s be honest, it can get pretty messy. Now, let’s switch gears and I’ll show you how to create your own matcher in Jest.
Jest equips us with the expect.extend()
API, an excellent tool for crafting both symmetric and asymmetric matchers. The process is quite straightforward: just provide an object where keys are your matcher names and values are the matcher functions.
The structure of a Custom Matcher
Every custom matcher function in Jest should return an object with two vital properties: pass
, a boolean indicating the test result, and message
, a function that returns a string message in case of a test failure.
interface CustomMatcherResult {
pass: boolean;
message: () => string;
}
Remember, Jest’s .not
modifier? It flips the pass
value, so it’s wise to craft messages for both possible outcomes. For example:
if (isValid) {
return {
pass: true,
message: () => 'Expected value not to be valid'
};
} else {
return {
pass: false,
message: () => 'Expected value to be a valid'
};
}
Your function’s first parameter is the value under test, with additional parameters added as needed. For a toBeEqualString
matcher to compare two strings, the signature would look like this:
function toBeEqualString(actual: string, expected: string)
And for a matcher verifying if a number is within a range:
function toBeWithin(actual: number, min: number, max: number)
In our case of validating GPS Coordinates, no extra parameters are needed. Thus, the toBeValidGPSCoordinate()
matcher setup might look like this:
import { expect } from "@jest/globals";
expect.extend({
toBeValidGPSCoordinate(actual: GPSCoordinate) {
let isValid = false;
// Check if the actual value is a GPSCoordinate object
const isGPSCoordinate = typeof actual === "object" &&
typeof actual.latitude === "number" &&
typeof actual.longitude === "number";
if (!isGPSCoordinate) {
throw new Error("Actual value should be a GPSCoordinate object");
}
const { latitude, longitude } = actual;
const hasValidLatitude = latitude >= -90 && latitude <= 90;
const hasValidLongitude = longitude >= -180 && longitude <= 180;
isValid = hasValidLatitude && hasValidLongitude;
if (isValid) {
return {
pass: true,
message: () => `Expected ${JSON.stringify(actual)} not to be a valid GPS Coordinate`
};
} else {
return {
pass: false,
message: () => `Expected ${JSON.stringify(actual)} to be a valid GPS Coordinate`
};
}
}
});
custom-matchers.tsAnd that’s it! This structure makes your custom matchers ready for action. You can then refactor your test file like this:
const inputs = [
// Define satellite and motion information
// ...
];
test.each(inputs)("should produce valid GPS coordinates", async (input) => {
const mockAPIRequest = jest.spyOn(APIService, 'requestInformationFromSatellite');
mockAPIRequest.mockResolvedValue(input.satelliteInfo);
const output = await computeGPSCoordinate(input.motionInfo);
expect(output).toBeValidGPSCoordinate();
});
It’s pretty straightforward, isn’t it? Now, you can reuse this validation logic in several other places and ace your role as a Node.js Backend Engineer at Uber! 😅
Where to save your matchers
You can register your matcher in any test file. For one-off uses, there’s no need for a separate file. However, best practice suggests gathering your matchers in dedicated files and registering them in Jest’s config:
export default {
// Letting Jest know we’re loading some custom matchers.
setupFilesAfterEnv: ['./custom-matchers.ts'],
}
jest.config.tsHow to make a matcher type-safe with TypeScript
To inform TypeScript about your extended Jest function, extend Jest’s type definitions. Create a jest.d.ts
file and extend the jest.Matchers
and jest.AsymmetricMatchers
interfaces as needed.
declare global {
namespace jest {
// Register as a Symmetric Matcher
interface Matchers<R> {
toBeValidGPSCoordinate(actual: GPSCoordinate): R;
}
// Optionally, also as an Asymmetric Matcher
interface AsymmetricMatchers {
toBeValidGPSCoordinate(actual: GPSCoordinate): void;
}
}
}
jest.d.tsEnsure this type definition file is included in your tsconfig.json
:
{
"include": ["jest.d.ts"]
}
tsconfig.jsonConclusion: Embracing the Power of Custom Matchers
We’ve just explored the depths of creating custom matchers in Jest, an endeavor that opens up a world of possibilities for simplifying and streamlining our tests. By extending Jest’s capabilities, we can tackle complex validations with ease, making our test suites not only more efficient but also more readable.
As we wrap up this section, remember that the true power of custom matchers lies in their ability to encapsulate intricate logic and present it in a clear, concise manner. This approach not only enhances the maintainability of our tests but also aids in quicker debugging and better understanding among team members.
Up next, we’re going to dive into adding some flair to our custom matchers. We’ll look at how to use Jest’s helpers to add color and proper formatting to error messages, making them more informative and easier to decipher. Additionally, we’ll discuss handling invalid parameter values gracefully, ensuring our custom matchers are robust and user-friendly. Stay tuned to elevate your testing game to the next level!