Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom matcher in jest

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.

like image 968
rober710 Avatar asked Aug 17 '17 19:08

rober710


People also ask

What are matchers in Jest?

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.

How do you match objects in Jest?

With Jest's Object partial matching we can do: test('id should match', () => { const obj = { id: '111', productName: 'Jest Handbook', url: 'https://jesthandbook.com' }; expect(obj). toEqual( expect. objectContaining({ id: '111' }) ); });

What is a matcher in testing?

A Matcher is the Kotest term for an assertion that performs a specific test.

What is the difference between toBe and toEqual?

toBe and compares value: The biggest difference is its behavior with objects. . toEqual looks at key value pairs and tests for equality, rather than making sure they are truly the same object (same reference in memory). And that's about it!


2 Answers

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.

like image 82
Michael Jungo Avatar answered Nov 07 '22 15:11

Michael Jungo


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)
})
like image 4
tuomassalo Avatar answered Nov 07 '22 13:11

tuomassalo