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.

[expect] Allow creation of custom asymmetric matchers.

See original GitHub issue

🚀 Feature Proposal

This is a dog food feature request for the expect package.

Expect’s internal matchers are defined using a powerful but private API: classes extending AsymmetricMatcher. While it is currently possible to import AsymmetricMatcher from expect’s internal modules, it is not documented and expect.extend does not understand matchers defined as AsymmetricMatcher derivates. See this line where all non-internal matchers are assumed to be functions and wrapped in an AsymmetricMatcher.

I propose to document AsymmetricMatcher, give it an export symbol from the expect package, and modify expect.extend to wrap function type matchers in CustomMatcher (as is done already), while not performing any wrapping on matchers which extend AsymmetricMatcher.

Motivation

Providing complex named assertion types is done to facilitate the best possible messages for developers (and non developers!) when things break. By allowing matcher authors to create new asymmetric matchers they can better fulfill their goals, making Jest more powerful (without being any more complex) for end users.

Example

I am building assertions helpers for iterators. For example, I would like create expect(iterable).toBeIterable()

It should pass if Symbol.iterator exists and is a function. not.ToBeIterable should pass only if Symbol.iterator does not exist.

Pitch

As proposed Jest API surface, this can’t NOT be in the core platform.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:24 (11 by maintainers)

github_iconTop GitHub Comments

4reactions
conartist6commented, Jul 7, 2018

OK here’s what I’ve got so far:

expect.extend({
  toBeIterable(received, argument) {
    let pass = Symbol.iterator in received;
    const iterator = received[Symbol.iterator];

    if (pass && argument && typeof argument.asymmetricMatch === 'function') {
      return {pass: argument.asymmetricMatch(iterator.call(received))};
    }

    return {
      message: () =>
        this.utils.matcherHint(`${pass ? '.not': ''}.toBeIterable`) +
        '\n\n' +
        `expected${pass ? ' not' : ''} to find a [Symbol.iterator] property, but Symbol.iterator was:\n` +
        `  ${this.utils.printReceived(received)}`,
      pass,
    };
  },

  yields(received, ...args) {
    let pass = args.reduce((pass, arg, i) => {
      return pass && arg === received.next().value;
    }, true)
    const done = received.next();
    pass = pass && done.done;
    return {
      message: () => `Didn't do the thing.`,
      pass,
    }
  },
})
function* iter() {
  yield 2;
  yield 4;
  yield 6;
}
expect(iter).toBeIterable(expect.yields(2, 4, 6))

This code works, (succeeds and fails in the correct situations), however the failure message of the inner expect is lost. I don’t fully understand, from the perspective of Jest’s design, why the message would be discarded. I’m presuming it has to do with matching the Jasmine API.

Note: this required a Jest API change already in order for argument.asymmetricMatch to receive the variadic arguments passed to yields.

3reactions
LukasBombachcommented, Sep 17, 2019

Hey guys, reanimating this old issue.

I came across this because I have this data structure (which I have already simplified for this issue):

type DiscoverEvent = [
  string,
  boolean,
  number,
  Advertisement,
];

interface Advertisement {
  localName?: string;
  txPowerLevel?: number;
  manufacturerData?: Buffer;
  serviceData?: Buffer;
}

So when I write my tests I want to see if my data matches my expected data:

const data = [
  "ae3f",
  true,
  4,
  {
    localName: "foobar"
  }
]

expect(data).toEqual(          
  expect.arrayContaining([      
    expect.any(String),
    expect.any(Boolean),
    expect.any(Number),
    expect.objectContaining({
      // difficult
    })
  ])
);

So the problem is that I’d like to use an asymmetric matcher that I can use within arrayContaining. As I understand it, I could not just use jest.extend because I could only write a custom matcher with that, like

expect(data).toBeAdvertisement();

which would work on its own, but I could not do this:

expect(data).toEqual(          
  expect.arrayContaining([      
    expect.any(String),
    expect.any(Boolean),
    expect.any(Number),
    expect(data[3]).toBeAdvertisement()
  ])
);

right? So to validate a data structure like this I would need a mechanism to write a matcher that can be used alongside with arrayContaining or objectContaining

Read more comments on GitHub >

github_iconTop Results From Across the Web

Practical Guide to Custom Jest Matchers - Redd Developer
Creating a custom matcher. Jest provides the expect.extend() API to implement both custom symmetric and asymmetric matchers.
Read more >
How To Improve Your Tests with Custom Matchers
Jest allows us to add your own matchers via its expect.extend method. The actual implementation uses expect.objectContaining and expect.
Read more >
Expect - Jest
Custom Matchers API​​​ Matchers should return an object (or a Promise of an object) with two keys. pass indicates whether there was a...
Read more >
Custom Jasmine Asymmetric Matchers - YIOU CHEN
We can create a custom asymmetric matcher that normalize the difference before doing the comparison.
Read more >
Create Custom Jest Matchers to Test Like a Pro
You can see that toBeISODate is a function that accepts a single value, received . You can accept more arguments, these will be...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

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