I would like to extend Jest's isEqual
matcher so that the expected value is transformed before comparison (this allows me to use multiline strings in tests). All I need to do is run the expected value through a the indentToFirstLine
function from the lib: indent-to-first-line
before passing it to isEqual
. Obviously I don't want to have to do this everywhere I need it, so it makes sense to fold this into a matcher, and as I want identical functionality to Jest / Expect's isEqual
matcher, it makes sense to utilise that.
I've tried the following:
import indentToFirstLine from 'indent-to-first-line'
import expect from 'expect'
const toEqualMultiline = (received, expectedTemplateString) => {
const expected = indentToFirstLine(expectedTemplateString)
return expect(received).toEqual(expected)
}
export default toEqualMultiline
However expect(received).toEqual(expected)
doesn't return a value, so the value returned from my matcher in undefined
, causing Jest to error:
Unexpected return from a matcher function. Matcher functions should return an object in the following format: {message?: string | function, pass: boolean} 'undefined' was returned
Is it possible for me to use toEqual
from within my own matcher?
When you're writing tests, you often need to check that values meet certain conditions. expect gives you access to a number of "matchers" that let you validate different things. For additional Jest matchers maintained by the Jest Community check out jest-extended .
Jest uses "matchers" to let you test values in different ways. This document will introduce some commonly used matchers. For the full list, see the expect API doc.
This is from Jest documentation: Expect. assertions(number) verifies that a certain number of assertions are called during a test. This is often useful when testing asynchronous code, in order to make sure that assertions in a callback actually got called.
The toBeGreaterThan and toBeLessThan matchers check if something is greater than or less than something else.
You can use expect.extend()
to do just that. If you're using create-react-app, you can put this example code below in the setupTests.ts
so it can be applied to all of the tests you run:
expect.extend({
toBeWithinRange(received, min, max) {
const pass = received >= min && received <= ceiling
return {
message: () =>
`expected ${received} to be in range ${floor} - ${ceiling}`,
pass,
}
},
})
it('should fail', () => {
expect(13).toBeWithinRange(1, 10)
})
When running the test above this is the output:
But we can do better than that. Look at how the built-in matchers display the error message:
As you can see the error is easier to read because the expected and received values have different colors and there is a matcher hint above to denote which one is which.
To do that we need to install this package jest-matcher-utils
and import a couple of methods to pretty print the matcher hint and values:
import { printExpected, printReceived, matcherHint } from "jest-matcher-utils"
const failMessage = (received, min, max) => () => `${matcherHint(
".toBeWithinRange",
"received",
"min, max"
)}
Expected value to be in range:
min: ${printExpected(min)}
max: ${printExpected(max)}
Received: ${printReceived(received)}`
expect.extend({
toBeWithinRange(received, min, max) {
const pass = received >= min && received <= max
return {
pass,
message: failMessage(received, min, max),
}
},
})
Now it looks way better and can help you identify the problem quicker
However there is a small bug in the code above, when you negate the assertion
expect(3).not.toBeWithinRange(1, 10)
The output is .toBeWithinRange
instead of .not.toBeWithinRange
:
expect(received).toBeWithinRange(min, max)
Expected value to be in range:
min: 1
max: 10
Received: 3
To fix that, you can conditionally add the negative word based on the pass
value
const failMessage = (received, min, max, not) => () => `${matcherHint(
`${not ? ".not" : ""}.toBeWithinRange`,
"received",
"min, max"
)}
Expected value${not ? " not " : " "}to be in range:
min: ${printExpected(min)}
max: ${printExpected(max)}
Received: ${printReceived(received)}`
toBeWithinRange(received, min, max) {
const pass = received >= min && received <= max
return {
pass,
message: failMessage(received, min, max, pass),
}
},
Now rerun the test again, you will see this:
Pass if false
expect(3).not.toBeWithinRange(1, 10)
expect(received).not.toBeWithinRange(min, max)
Expected value not to be in range:
min: 1
max: 10
Received: 3
Pass if true
expect(13).toBeWithinRange(1, 10)
expect(received).toBeWithinRange(min, max)
Expected value to be in range:
min: 1
max: 10
Received: 13
If you are using jest and passing that matcher to expect.extend, you can use the provided execution context to execute the jest equals method like this:
import indentToFirstLine from 'indent-to-first-line'
export default function toEqualMultiline(received, expectedTemplateString) {
const expected = indentToFirstLine(expectedTemplateString);
return {
message: () => `expected ${received} to equals multiline ${expected}`,
pass: this.equals(received, expected)
};
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With