Skip to content
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

Implement a matcher API for "loosely" asserting on deep properties #644

Closed
keithamus opened this issue Mar 18, 2016 · 20 comments
Closed

Implement a matcher API for "loosely" asserting on deep properties #644

keithamus opened this issue Mar 18, 2016 · 20 comments

Comments

@keithamus
Copy link
Member

We have come across this discussion a few times, but for many people deep.equal is too-strict of an assertion. For example:

The proposal is thus:

Using deep.equal or similar complex assertions, we are able to make much looser assertions about specific parts of the assertion, using an assertion sentinel in place of a value:

expect(o).to.deep.equal({
  foo: 'bar',
  baz: chai.match.a('function').with.length(2),
});

expect(someSpy).to.be.calledWith('foo', chai.match.a('string').that.matches(/bar/));
@meeber
Copy link
Contributor

meeber commented Apr 29, 2016

I like the idea behind this. Where do you envision this falling on the roadmap?

The only thing I'm not crazy about is the term "match", simply because of its existing association with regular expressions.

I haven't yet put any thought into the feasibility of this or anything resembling this, but just pie-in-the-skying, it'd be pretty neat if the syntax resembled whatever interface the developer was using:

expect(o).to.deep.equal({
  foo: 'bar',
  baz: expect.a('function').with.length(2),
});

o.should.deep.equal({
  foo: 'bar',
  baz: should.be.a('function').with.length(2),
});

@keithamus
Copy link
Member Author

I'm doing some work on deep-eql - extending it to take an optional comparator function, which will go a lot of the way to providing this kind of functionality.

@Turbo87
Copy link
Contributor

Turbo87 commented May 18, 2016

As mentioned in #709 I'd like to have some sort of high-level assertion that enables me to deep equal two JSON objects while using something like closeTo() for comparing numbers. I'm not quite sure if/how this new feature might help with that, but I'd be happy to discuss it... 😉

@keithamus
Copy link
Member Author

As @Turbo87 mentioned in #709, deep equal could, for example, take a second arg of a comparitor

function compare(a, b) {
  if (typeof a === 'number' && typeof b === 'number') {
    // using the expect() semantics here might not be the best idea...
    expect(a).to.be.closeTo(b, 0.0001);
  } else {
    return this._super(a, b);
  }
}
expect(result).to.deep.equal(expected, compare);

Which fits the work we're doing in deep-eql to power this feature.

@shahata
Copy link

shahata commented Oct 12, 2017

Did Chai eventually add support for this? I couldn't find any docs around it.. cc @keithamus

@keithamus
Copy link
Member Author

We've not added it yet. It's on my agenda after better error messaging though. Can't promise timelines but it will be coming!

@Jonarod
Copy link

Jonarod commented Dec 20, 2017

I have been through all listed discussions and each of them make propositions to permanently include new assertions into chai, but I can't find any temporary hacks/solutions.

How could I pass a test comparing two objects each containing an anonymous function like this:

var inputObj = {name: 'input', fn: () => ({key: 'value'})}
var expectedObj = {name: 'input', fn: () => ({key: 'value'})}

expect( inputObj ).to.deep.equal( expectedObj )  

The only hack I found is this:

try {
  expect( inputObj ).to.deep.equal( expectedObj )  
} 
catch (e) {
  console.log( JSON.stringify(e.actual) === JSON.stringify(e.expected) )
}

But this is really too much of a hack for me. Any other ideas ?

@mhuggins
Copy link

mhuggins commented Mar 18, 2018

@Jonarod's solution is the only seemingly close solution for keeping this code clean (shy of switching to Jest). It falls short though when objects keys are in a different order.

@Jonarod
Copy link

Jonarod commented Mar 18, 2018

@mhuggins my "solution" is not even one... it's just a way to have some alerts and visually compare stuff. But I don't recommend this for serious tests plans...
If you already know the result of your function (in my example I know it) you should have a better bet at comparing result of your function for your expectedObj, like:

var inputObj = {name: 'input', fn: () => ({key: 'value'})}
var expectedObj = {name: 'input', fn: () => ({key: 'value'})}

// compare strings without functions
expect( JSON.stringify(inputObj) ).to.deep.equal( JSON.stringify(expectedObj) )  

// compare functions results
expect( inputObj.fn() ).to.deep.equal( expectedOb.fn() )  

yet, this is just lame workaround and won't work in 100% cases...

@besh
Copy link

besh commented Apr 30, 2018

@keithamus just wondering about the status. Can a better timeline be given at this point?

@keithamus
Copy link
Member Author

the deep-eql lib has a comparator function now, so the ground work has been laid. We have some things higher on the list to tackle first, though. If anyone is interested in picking up this (big) change I can spend some time writing down how I think it should be architected which should get you going with it.

@d-damien
Copy link

d-damien commented Jun 5, 2018

@keithamus I can't find any example in documentation. Is it possible to write something like this ?

expect(1.0001).to.deep.equal(1, (a, b) => Math.abs(a - b) < 1e-3)

@keithamus
Copy link
Member Author

@d-damien it is not possible right now, as the second argument is not passed down to deep-eql as a comparator. If someone wanted to make a PR to this, then it could be made possible.

@keithamus
Copy link
Member Author

keithamus commented Jun 9, 2018

This is now in our roadmap https://github.com/chaijs/chai/projects/2.

@oleg-codaio
Copy link

oleg-codaio commented Jun 27, 2018

While folks are waiting on this, there's actually an easy workaround using Sinon matchers.

The implementation is something like this:

// Rewrite as needed wherever you set up your custom assertion functions
// (You'll have to adapt for BDD style if you use that.)
import chai from 'chai';
import sinon from 'sinon';

chai.assert.matching = sinon.match;

chai.assert.matchEqual = function(a: object, b: any): void {
  const fakeSpy = sinon.spy();
  fakeSpy(a);
  sinon.assert.calledWith(fakeSpy, b);
};

And then you can just use this new assertion.

// Now, call like this.
const result = {val: 123, bar: 'foo'}; // Returned by method being tested.
assert.matchEqual(result, {val: assert.matching.number, bar: 'foo'});

Of course the error messages are a bit weird since they're something like AssertError: expected spy to be called with arguments, but I think this is a solid stopgap solution until Chai has its own matchers. Hope this helps!

@jpbochi
Copy link

jpbochi commented Jun 28, 2018

@vaskevich I like that approach a lot. You should be able to implement it using chai.Assertion.addMethod, and then do expect(o).to.match({ ... })

@keithamus
Copy link
Member Author

the plan is to have this behaviour in Chai 5. You're welcome to make plugins that accept Sinon matchers for now - but be warned it may be obsolete by the time Chai 5 comes out 😃

@jpbochi
Copy link

jpbochi commented Jun 28, 2018

I ended up implementing @vaskevich approach as a plugin. It even replaces this potentially confusing text about stubs being called. Here it is:

chai.use((_chai, utils) => {
  chai.Assertion.addMethod('matchEql', function fn(expectedMatch) {
    const subject = utils.flag(this, 'object');
    const stub = sinon.stub();
    stub(subject);
    try {
      sinon.assert.calledWithMatch(stub, expectedMatch);
    } catch (error) {
      error.name = 'MatchAssertionError';
      error.message = error.message.replace(
        /^expected stub to be called with match/,
        `expected ${utils.objDisplay(subject)} to match`
      );
      throw error;
    }
  });
});

@mrsufgi
Copy link

mrsufgi commented Nov 26, 2018

@jpbochi works great, finally, I could replace my deep.equal with your plugin and assert objects that contain functions! :D

@frosas
Copy link

frosas commented Nov 7, 2020

For those considering Sinon, you probably want to use sinon.assert.match()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests