Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is It Possible To Extend A Jest / Expect Matcher

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?

like image 759
Undistraction Avatar asked Feb 24 '18 19:02

Undistraction


People also ask

Is expect part of Jest?

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 .

What is a Jest matcher?

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.

What is expect assertions in Jest?

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.

Which matcher function is tested greater than condition?

The toBeGreaterThan and toBeLessThan matchers check if something is greater than or less than something else.


2 Answers

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,
    }
  },
})

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
like image 136
NearHuscarl Avatar answered Sep 21 '22 01:09

NearHuscarl


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)
    };
}
like image 30
mgarcia Avatar answered Sep 22 '22 01:09

mgarcia