14

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?

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Undistraction
  • 42,754
  • 56
  • 195
  • 331

2 Answers2

16

You can use expect.extend() to do just that. If you're using , 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,
    }
  },
})

Usage

it('should fail', () => {
  expect(13).toBeWithinRange(1, 10)
})

When running the test above this is the output:

test1

But we can do better than that. Look at how the built-in matchers display the error message:

test2

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

test3

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
NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
  • you helped me provide [this answer](https://stackoverflow.com/questions/45348083/how-to-add-custom-message-to-jest-expect/67803154#67803154) – piouson Jun 02 '21 at 10:33
  • 2
    Unfortunately this doesn't _fully_ answer the question. It leaves the topic of how to augment an existing Jest matcher unanswered. For example, I'd like to add a custom matcher to augment the `toHaveBeenCalledWith` built-in matcher by asserting that an expected argument is present, like `return expect(fn).toHaveBeenCalledWith(a, b, { foo: "bar" })` ...but that results in the error message OP noted. And I'd prefer to avoid completely re-implementing `expect(fn).toHaveBeenCalledWith`. – RJ Felix Nov 07 '22 at 22:30
2

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)
    };
}
mgarcia
  • 5,669
  • 3
  • 16
  • 35