Writing Tests for Custom Matchers in Jest
Daniel
Posted on July 7, 2022
TLDR: Here's an example of how to write a test for a custom matcher:
describe("toHaveDevDependency", () => {
it("fails when given a number", () => {
expect(() => {
expect(2).toHaveDevDependency("node");
}).toThrow("Expected 2 to be a YeomanTest.RunResult");
);
});
Motivation
In the spirit of TDD, I found myself wanting to write tests to ensure my Jest custom matchers work properly.
A quick Google Search for "Jest write tests for custom matcher" only produced results about writing custom matchers themselves when really I wanted to see examples of how to write tests that exercise my custom matchers.
After figuring out how to do this on my own, I thought I'd write the missing article :).
Background
If you're familiar with Jest, you're likely also familiar with custom matchers.
If not, here's the summary version: custom matchers allow you to add your own matching assertions for expect
statements. This can be super useful when you find yourself writing the same testing code over and over again and want to DRY up your tests.
Example Situation
This happened recently when writing a Yeoman generator to quickly scaffold new projects. I wanted to write something like expect(result).toHaveDevDependency("typescript")
to assert that the package.json
file generated with the project includes a specified package in its devDependencies
.
The testing helpers provided by Yeoman in yeoman-test
include a useful assertJsonFileContent(fileName: string, content: any)
method, but as I found myself writing things like
result.assertJsonFileContent(
"package.json",
{
devDependencies: {
typescript: "4.7.4"
}
},
);
over and over I got tired of the verbosity and realized that this line doesn't state my intent as clearly as it could.
I wanted something like this instead:
expect(result).toHaveDevDependency("typescript", "4.7.4");
Simple, clear, and clean.
My toHaveDevDependency
custom matcher ended up being reasonably complicated, including checks to ensure that the received object in the expect
call was, indeed, a YeomanTest.RunResult
object, and then wrapping the result.assertJsonFileContent()
call with a try
/catch
to translate that result into either a passing or failing jest.CustomMatcherResult
with a useful message.
Ironically, the custom matcher ended up showing up in my code coverage reports as the least tested part of my codebase.
The Solution
I wrote myself a test suite for toHaveDevDependency
and in the process, I had to figure out how to test an expect
statement itself. Rather meta.
First Attempt: wrap expect
call in a try
/catch
My first attempt looked something like this:
describe("toHaveDevDependency", () => {
it("fails when given a number", () => {
try {
expect(2).toHaveDevDependency("node");
} catch (error) {
expect(error.message).toContain("JestAssertionError");
}
);
});
The idea being that Jest is likely throwing some kind of error when an expect
assertion fails, and perhaps I can catch that error and then do something with it.
Unfortunately, this does not work. Jest seems to be smart enough to fail the test immediately, and while I can catch the error, I couldn't find an easy way to stop the test from failing down this path. There might be a way, but I gave up before I found it.
Second Attempt: wrap raw expect
call with expect().toThrow()
My next idea was to use the expect().toThrow()
method to tell Jest explicitly that I'm expecting an error throw. It looked like this:
describe("toHaveDevDependency", () => {
it("fails when given a number", () => {
expect(
expect(2).toHaveDevDependency("node");
).toThrow("Expected 2 to be a YeomanTest.RunResult");
);
});
This didn't work, either. The test failed immediately.
Final Working Attempt: wrap expect
call in an arrow function and wrap that in an expect().toThrow()
I had one more idea: what if I wrap the expect
call in an arrow function first?
This looked like this:
describe("toHaveDevDependency", () => {
it("fails when given a number", () => {
expect(() => {
expect(2).toHaveDevDependency("node");
}).toThrow("Expected 2 to be a YeomanTest.RunResult");
);
});
This works! I haven't looked into the Jest internals enough to understand exactly why, but intuitively this makes sense: the arrow function insulates the inner call to expect
from the Jest test context, and prevents the function call from being executed immediately within the it
block.
Now I can happily write test suites for my custom matchers and test everything with confidence!
Posted on July 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.