Chai: Implement a matcher API for "loosely" asserting on deep properties

Created on 18 Mar 2016  Â·  20Comments  Â·  Source: chaijs/chai

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

  • #97 discusses Sinon matchers, and having something similar.
  • #324 went a bit further, including showing how Jasmine handles this with jasmine.any.
  • This may help with issues like #193
  • This would help to solve issues like #643 (and perhaps tangentially, #426)
  • Personally I've run into small problems with this stuff before, with things like asserting arguments with chai-spies

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/));
more-discussion-needed âž¡ deep-equal

Most helpful comment

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!

All 20 comments

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),
});

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.

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... 😉

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.

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

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!

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 ?

@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.

@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...

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

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.

@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)

@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.

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

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!

@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({ ... })

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 😃

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;
    }
  });
});

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

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ghost picture ghost  Â·  4Comments

leifhanack picture leifhanack  Â·  4Comments

liborbus picture liborbus  Â·  4Comments

meeber picture meeber  Â·  3Comments

domenic picture domenic  Â·  4Comments