question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Expose matchers in expect.extend

See original GitHub issue

🚀 Feature Proposal

Expose existing matchers inside expect.extend.

Motivation

Sometimes you want the existing functionality of a matcher but you want to it to transform the input before doing so, for instance, to ignore some specific keys of an object.

Writing a custom matcher is extremely verbose and requires importing additional packages to maintain the same quality of the core matchers (diff in messages).

For example, if I want a matcher that performs toEqual on two objects but ignores a single property on those objects:

expect.extend({
  toEqualDesign(recieved, expected, extraMatchers = []) {
    const recievedDesign = { ...recieved, change: null };
    const expectedDesign = { ...expected, change: null };
    const pass = this.equals(recievedDesign, expectedDesign, extraMatchers);

    // Duplicated from jest.
    // https://github.com/facebook/jest/blob/f3dab7/packages/expect
    // /src/matchers.ts#L538-L569
    /* eslint-disable */
    const matcherName = 'toEqualDesign';
    const options = {
      comment: 'design equality',
      isNot: this.isNot,
      promise: this.promise,
    };
    const message = pass
      ? () =>
          matcherHint(matcherName, undefined, undefined, options) +
          '\n\n' +
          `Expected: ${printExpected(expectedDesign)}\n` +
          `Received: ${printReceived(recievedDesign)}`
      : () => {
          const difference = diff(expectedDesign, recievedDesign, {
            expand: this.expand,
          });

          return (
            matcherHint(matcherName, undefined, undefined, options) +
            '\n\n' +
            (difference && difference.includes('- Expect')
              ? `Difference:\n\n${difference}`
              : `Expected: ${printExpected(expectedDesign)}\n` +
                `Received: ${printReceived(recievedDesign)}`)
          );
        };

    return {
      actual: recievedDesign,
      expected: expectedDesign,
      message,
      name: matcherName,
      pass,
    };
  },
});

Example

return expect.extend({
  toEqualDesign(recieved, expected, ...args) {
    const recievedDesign = { ...recieved, change: null };
    const expectedDesign = { ...expected, change: null };
    return {
      ...this.matchers.toEqual(recievedDesign, expectedDesign, ...args)
      name: 'toEqualDesign',
    };
  },
});

and then:

expect(a).toEqualDesign(b);
expect(a).not.toEqualDesign(b);

Pitch

I am aware this has been asked for before:

The response was to use expect.extend and I do not think it considers these cases where using expect.extend as it stands is not only massively inconvenient upfront for such a simple comparison but creates longer term debt having to maintain the matcher, whereas leveraging the return value of a core matcher allows your matcher to benefit from the continued maintenance of it in the jest core, e.g., if it gets improved messages or the already very verbose matcher return API changes.

This proposal is to enable the ability to write matchers that don’t want to introduce new matching behaviour but want to transform their inputs before matching.

Other alternatives include:

expectToEqualDesign(a, b) {
   expect({ ...a, change: null }).toEqual({ ...b, change: null });
}

You then have to handle not yourself by either making separate functions or flagging it:

expectToEqualDesign(a, b, { not: false } = {}) {
   let expectation = expect({ ...a, change: null });
   if (not) {
       expectation = expectation.not;
   }
   expectation.toEqual({ ...b, change: null });
}

Which will work, but now requires you to know an entirely different syntax because of a slight difference to the matcher.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:20
  • Comments:5

github_iconTop GitHub Comments

5reactions
stephenhcommented, Apr 15, 2021

I ended up here from #2547, and @SimenB you’d asked for use cases in that one (…admittedly ~3 years ago 😄), but similar to @georeith I want to make a custom matcher that a) accepts args, b) does some pre-processing, and then c) hands off to an existing matcher, in my case toMatchObject to leverage it’s great out-of-the-box formatting/diffing/etc capabilities.

Basically, in our project, the actual instance that is passed to my expect(actual).toMatchObject({ ... }) has ugly implementation details that I want to clean up (almost like a .toJSON to get it to be “just data”) for the toMatchObject.

In my case I’m using a require hack for now:

export async function toMatchEntity<T>(actual: Entity, expected: MatchedEntity<T>): Promise<CustomMatcherResult> {
  // Clean up `actual` to be "just data"
  const copy = ...project specific stuff...

  // Blatantly grab `toMatchObject` from the guts of expect
  const { getMatchers } = require("expect/build/jestMatchersObject");

  // Now use `toMatchObject` but with our "just data" version of `actual`
  return getMatchers().toMatchObject.call(this, copy, expected);
}

With @georeith 's proposal, the require hack would go away and this could become:

return expect.extend({
  toMatchEntity(actual, expected) {
    const copy = ...same clean up...;
    return this.matchers.toMatchObject(copy, expected);
  },
});
1reaction
stephenhcommented, Apr 29, 2021

@bpinto hm, no, I haven’t tried to re-use objectContaining yet, so I’m not sure how/if it would be different.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Expect
expect(value) · expect.extend(matchers) · expect.anything() · expect.any(constructor) · expect.arrayContaining(array) · expect.assertions(number) ...
Read more >
Practical Guide to Custom Jest Matchers - Redd Developer
Jest provides the expect.extend() API to implement both custom symmetric and asymmetric matchers. This API accepts an object where keys ...
Read more >
Extending Matchers | Guide
To extend default matchers, call expect.extend with an object containing your matchers. ts
Read more >
Expect · Jest
expect(value) · expect.extend(matchers) · this.isNot · this.utils · expect.anything() · expect.any(constructor) · expect.arrayContaining(array) · expect.assertions( ...
Read more >
expect.extend(matchers) - 《Jest v24.1 documentation》
expect.extend also supports async matchers. Async matchers return a Promise so you will need to await the returned value. Let's use an example ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found