Custom matcher in jest

14,423

Solution 1

There are two different kinds of methods that are related to expect. When you call expect(value) you get an object with matchers methods that you can use for various assertions (e.g. toBe(value), toMatchSnapshot()). The other kind of methods are directly on expect, which are basically helper methods (expect.extend(matchers) is one of them).

With expect.extend(matchers) you add the first kind of method. That means it's not available directly on expect, hence the error you got. You need to call it as follows:

expect(string).stringMatchingOrNull(regexp);

But when you call this you'll get another error.

TypeError: expect(...).stringMatching is not a function

This time you're trying to use use expect.stringMatching(regexp) as a matcher, but it is one of the helper methods on expect, which gives you a pseudo value that will be accepted as any string value that would match the regular expression. This allows you to use it like this:

expect(received).toEqual(expect.stringMatching(argument));
//     ^^^^^^^^          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//      string                   acts as a string

This assertion will only throw when it fails, that means when it's successful the function continues and nothing will be returned (undefined) and Jest will complain that you must return an object with pass and an optional message.

Unexpected return from a matcher function.
Matcher functions should return an object in the following format:
  {message?: string | function, pass: boolean}
'undefined' was returned

One last thing you need to consider, is using .not before the matcher. When .not is used, you also need to use .not in the assertion you make inside your custom matcher, otherwise it will fail incorrectly, when it should pass. Luckily, this is very simple as you have access to this.isNot.

expect.extend({
    stringMatchingOrNull(received, regexp) {
        if (received === null) {
            return {
                pass: true,
                message: () => 'String expected to be not null.'
            };
        }

        // `this.isNot` indicates whether the assertion was inverted with `.not`
        // which needs to be respected, otherwise it fails incorrectly.
        if (this.isNot) {
            expect(received).not.toEqual(expect.stringMatching(regexp));
        } else {
            expect(received).toEqual(expect.stringMatching(regexp));
        }

        // This point is reached when the above assertion was successful.
        // The test should therefore always pass, that means it needs to be
        // `true` when used normally, and `false` when `.not` was used.
        return { pass: !this.isNot }
    }
});

Note that the message is only shown when the assertion did not yield the correct result, so the last return does not need a message since it will always pass. The error messages can only occur above. You can see all possible test cases and the resulting error messages by running this example on repl.it.

Solution 2

I wrote this hack to use any of the .to... functions inside .toEqual, including custom functions added with expect.extend.

class SatisfiesMatcher {
  constructor(matcher, ...matcherArgs) {
    this.matcher = matcher
    this.matcherArgs = matcherArgs
  }
  asymmetricMatch(other) {
    expect(other)[this.matcher](...this.matcherArgs)
    return true
  }
}
expect.expect = (...args) => new SatisfiesMatcher(...args)

...

expect(anObject).toEqual({
  aSmallNumber: expect.expect('toBeLessThanOrEqual', 42)
})
Share:
14,423

Related videos on Youtube

rober710
Author by

rober710

Updated on September 16, 2022

Comments

  • rober710
    rober710 over 1 year

    I'm trying to create a custom matcher in Jest similar to stringMatching but that accepts null values. However, the docs don't show how to reuse an existing matcher. So far, I've got something like this:

    expect.extend({
        stringMatchingOrNull(received, argument) {
            if (received === null) {
                return {
                    pass: true,
                    message: () => 'String expected to be null.'
                };
            }
    
            expect(received).stringMatching(argument);
        }
    });
    

    I'm not sure this is the correct way to do it because I'm not returning anything when I call the stringMatching matcher (this was suggested here). When I try to use this matcher, I get: expect.stringMatchingOrNull is not a function, even if this is declared in the same test case:

    expect(player).toMatchObject({
        playerName: expect.any(String),
        rank: expect.stringMatchingOrNull(/^[AD]$/i)
        [...]
    });
    

    Please, can somebody help me showing the correct way to do it?

    I'm running the tests with Jest 20.0.4 and Node.js 7.8.0.

  • rober710
    rober710 over 6 years
    Oops! Thanks, @Michael, I forgot to write my intended usage of this matcher. I wanted to use it as a helper method inside the toMatchObject matcher. I've edited my question with the snippet that caused the error. From your answer, I assume using expect.extend is not the right way to do it. Are there any docs related to how to write helper functions for expect directly?
  • Michael Jungo
    Michael Jungo over 6 years
    I don't think there is an official way to create such a helper method that is providing a pseudo value. You could try to define one yourself like StringMatching. Either way, a value being null is an indication of a bug. If you explicitly set it to null you should have separate tests that cover these scenarios to make sure that it is only ever null when you expect it. Testing for a value or null sounds like you squash multiple tests together.
  • quezak
    quezak over 3 years
    @MichaelJungo how can I add my custom matcher to the second kind too, the helper methods called directly on expect? expect.extend() seems to only add it to the first kind, but the matchers available in the jest-extended package work directly on expect too, for example expect(o).toEqual({ aNumber: expect.toBeWithin(1, 3) }).
  • David Harkness
    David Harkness over 2 years
    @MichaelJungo Do you need to check this.isNot in the first check for null to reverse it or when returning pass: true?
  • Zyncho
    Zyncho almost 2 years
    please, add how to implement custom matcher class // #script.test.js expect(result).myCustomMatcherclass().method2(); // #myCustomMatcherclass.js class myCustomMatcherclass { constructor() {...} method2(){...} method2(){...} }