-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[expect] Allow creation of custom asymmetric matchers. #6649
Comments
I think we had an issue for that, but I'm on mobile so it's hard to find |
I searched all the open issues for "matcher" and didn't find anything in that list. |
Found it: #4711. |
Can we close this then? ;) |
Actually I can't tell why the other one was closed. They seem to have come to the same conclusion, that it would be necessary to expose and document the |
We added custom asymmetric matchers in Jest 23, is this different? |
Huuuuh? Those are symmetric matchers in my mind. While their messages can be different when invoked in inverse, there is only one condition. The asymmetric matchers I am referring to have an inverse (.not) success condition which isn't a boolean not of their success condition. An existing example of this is expect.objectContaining. |
Yeah these are the same thing 😄 Check out the implementation here: https://github.com/facebook/jest/pull/5503/files#diff-033f219e5ac882702ea47d090593655fR57. Looks like we map the API of Up for discussion on the limits/benefits of the API |
OK, I took some more time to see exactly what that feature is. I see now that it does allow asymmetry in matchers that are otherwise symmetric, but only with regard to matcher nesting. This does, I think, solve my 80% use case, and I see how it solved the use cases presented in the other issue. I still think that it makes sense to pursue this a bit further, because protocols have become a standard part of javascript life. In a protocol you use Symbols to attach data that fulfills a particular contract to an instance. For exmaple the Symbols are the key to this pattern because they make intent unambiguous. It isn't possible that, say, you're accessing a data object that just happens to have a property named typeof obj[Symbol.iterator] === function && obj[Symbol.iterator] While this is well and good, The matcher that checks for proper protocol implementation, however, must be fundamentally asymmetric. If the symbol is found, the remaining conditions of the protocol must be fulfilled in order for the protocol to be satisfied and the code to be correct. If the assertion is that the protocol is not present, then the symbol must not be found. I'm intrigued by the possibility, however, that what I'm suggesting is simply a new internal matcher along the lines of |
One possible version of
|
The argument against this is that there's no way to know whether a Symbol's value satisfies a particular contract. At most you'll be able to check the type of whatever's on there. To verify a This suggests that when writing tests about protocols, you should simply check for a protocol's Symbol if you want to know if the protocol is present (which is symmetric). If you have implemented the protocol yourself, you should test your implementation. |
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 |
@conartist6 but can you access any utils via |
I confirm that message from custom |
Also one might assume that messages were meant to be taken into account, according to this illustration. |
This looks more like a bug (error message not shown, if I understand correctly?) than a feature request - seeing as we do have |
Here: #7492 |
Perfect, thank you! |
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 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 |
I still really want this for iterables. To reiterate, I want it because when I write My use case is definitely not solved for yet. |
When you expect.extend asymmetric matchers get created for you although if the custom matcher is asynchronous it will not work. I am going to open an issue for this being mentioned in the documentation. You can just supply an asymmetric matcher object as argument - see my example. ( Unlike one that is created for you it is not 'not' aware ) In case you were not aware the arrayContaining is not strict, it does not care about order. Below is a custom matcher that works in your example. The difference between using a custom matcher as a custom matcher compared to an asymmetric matcher is that the this context is the matcher utils when used as a custom matcher.
|
Oh wow, what a detailed answer, thank you so much @tonyhallett |
@LukasBombach no problem although I was incorrect with part of my custom typing. I will revise. |
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
🚀 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 importAsymmetricMatcher
from expect's internal modules, it is not documented andexpect.extend
does not understand matchers defined asAsymmetricMatcher
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 theexpect
package, and modifyexpect.extend
to wrap function type matchers inCustomMatcher
(as is done already), while not performing any wrapping on matchers which extendAsymmetricMatcher
.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 ifSymbol.iterator
does not exist.Pitch
As proposed Jest API surface, this can't NOT be in the core platform.
The text was updated successfully, but these errors were encountered: