Custom equality testers
03 03 Problem (π solution)
Much like custom matchers, custom equality testers live in the setup file, which in our case is
vitest.setup.ts. To extend the built-in equality logic (the one used by .toEqual()), I will call the expect.addEqualityTesters() function and provide it with a list of custom equality testers.import { Measurement } from './src/measurement'
expect.addEqualityTesters([])
Every item in this list represents an equality tester function that Vitest will exhaust before treating two values as not equal.
I will declare a new tester called
measurementTester to teach Vitest how to compare Measurement instances.import { Measurement } from './src/measurement'
expect.addEqualityTesters([
function measurementTester(received, expected) {
// ...
},
])
Every equality tester accepts two arguments:
received, which is the actual value (the left-most part of the comparison);expected, which is the expected value (the right-most part).
This is how they translate to an assertion in your tests:
// π received π expected
expect(new Measurement(1, 'in')).toEqual(new Measurement(2.54, 'cm'))
// ^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
Vitest will automatically provide these two values to your equality tester function and expect it to return a boolean indicating whether these two values are equal.
Measurement comparison only becomes relevant if both the
received and expected values are instances of the Measurement class. Let's make sure to check that:import { Measurement } from './src/measurement'
expect.addEqualityTesters([
function measurementTester(received, expected) {
if (received instanceof Measurement && expected instanceof Measurement) {
// ...
}
},
])
Now, to the equality!
If you take a closer look at the implementation of the
Measurement class, it already has an .equals() method to help with the comparison.export class Measurement {
constructor(...) {}
public equals(other: Measurement): boolean {...}
}
So the actual value comparison becomes a matter of invoking that
.equals() method on any of the measurements provided by the tester:import { Measurement } from './src/measurement'
expect.addEqualityTesters([
function measurementTester(received, expected) {
if (received instanceof Measurement && expected instanceof Measurement) {
return expected.equals(received)
}
},
])
The values you will be comparing might not have the.equals()method (or not be classed at all!). Feel free to implement the comparison logic directly in your custom equality tester if that's the case.
Once this is done, I don't have to do anything extra for Vitest to respect my custom equality tester. In fact, if I run tests now, I will see them passing, which means Vitest can compare measurements correctly!
Equality testers vs Matchers
Let's iterate over the difference between equality testers and matchers to help you know when you need one over the other.
| Custom equality tester | Custom matcher |
|---|---|
Extends the .toEqual() logic. | Implement entirely custom logic. |
| Automatically applied recursively (e.g. if your measurement is nested in an object). | Always applied explicitly. Nested usage is enabled through asymmetric matchers ({ value: expect.myMatcher() }). |
| Must always be synchronous. | Can be both synchronous and asynchronous, utilizing the .resolves. and .rejects. chaining. |
Custom equality testers, as the name implies, are your go-to choice to help Vitest compare values that cannot otherwise be compared by sheer serialization (like our
Measurement, or, for example, Response instances).Custom matchers are generally more powerful and are reserved for expressing custom matching logic that can often be complex and compound (like reflecting a domain requirement in tests:
expect(user).toBeAuthenticated()).