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

Custom error assertion #113

Closed
yvele opened this issue Jun 29, 2015 · 11 comments
Closed

Custom error assertion #113

yvele opened this issue Jun 29, 2015 · 11 comments

Comments

@yvele
Copy link

yvele commented Jun 29, 2015

I would like to achieve the following with Chai expressivity:

if("should fail", function (done) {
  theFunctionToTest().then(
    function () {
      assert.fail("Expected to fail.");
      done();
    }, function (e) {
      expect(e).to.be.an.instanceof(MyError);
      expect(e.message).to.equal("Error message");
      expect(e.foo).to.deep.equal(["foo"]);
      done();
    }
  );
});

This works fine, but it lacks a way to make custom error assertions:

if("should fail", function () {
  return expect(theFunctionToTest())
    .to.be.rejectedWith(MyError);
});

Because of any assertion made in then will make the promise chain fail, you can't do something like that:

if("should fail", function (done) {
  return expect(theFunctionToTest().catch(function (e) {
    expect(e).to.be.an.instanceof(MyError);
    expect(e.message).to.equal("Error message");
    expect(e.foo).to.deep.equal(["foo"]);
    throw e;
  })).to.be.rejected;
});

Is there a way to custom assert my error?
If not, can we add a new feature? Like passing an assertion function to the rejectedWith function?

Let me know what do you think about that..

@domenic
Copy link
Collaborator

domenic commented Jul 1, 2015

This is indeed an area where Chai is not perfectly expressive (and thus Chai as Promised, which follows its API, is similar). This is the best I have:

it("should fail", function () {
  return theFunctionToTest().then(
    function () {
      assert.fail("Expected to fail.");
    },
    function (e) {
      expect(e).to.be.an.instanceof(MyError);
      expect(e.message).to.equal("Error message");
      expect(e.foo).to.deep.equal(["foo"]);
    }
  );
});

If you'd like something more elegant, what you need to do is ask Chai for a similar function. E.g. something like

it("should fail sync", function () {
  expect(theFunctionToTest).to.throwMatching(function (e) {
    expect(e).to.be.an.instanceof(MyError);
    expect(e.message).to.equal("Error message");
    expect(e.foo).to.deep.equal(["foo"]);
  });
});

which would allow you to eliminate the assert.fail.

@domenic domenic closed this as completed Jul 1, 2015
@keithamus
Copy link
Member

chaijs/chai#470 is a PR to implement breaking out the error checking components of chai into their own lib, which could theoretically be used to build up a throwMatching function. See also #47 of this repo which is looking to do something similar.

@yvele
Copy link
Author

yvele commented Jul 1, 2015

@keithamus Thanks for showing the way to implement a throwMatching
@domenic A throwMatching would be great.. I hope I'll find some time to make a PR.. PS: I like the way you transformed the e.foo assertions to a cleaner one line assertion in your sample ;)

@keithamus
Copy link
Member

In essence with the checkError functionality, you could write something like:

expect(theFunctionToTest).to.throwMatching(/Error message/);
expect(theFunctionToTest).to.throwMatching(new MyError('Error message'));
expect(theFunctionToTest).to.throwMatching(MyError);

@yvele
Copy link
Author

yvele commented Jul 1, 2015

@keithamus I don't get the point of your last post..

Chai.js already have those functionalities via throw:

expect(theFunctionToTest).to.throw(/Error message/);
expect(theFunctionToTest).to.throw(new MyError('Error message'));
expect(theFunctionToTest).to.throw(MyError);

And Chai as promised already support those via rejectedWith:

return expect(theFunctionToTest()).to.be.rejectedWith(/Error message/);
return expect(theFunctionToTest()).to.be.rejectedWith(new MyError('Error message'));
return expect(theFunctionToTest()).to.be.rejectedWith(MyError);

The only thing we need is something like throwMatching for Chai.js and rejectedMatching for Chai as Promised. Does that make sense?


Example of requested features for Chai.js:

expect(theFunctionToTest).to.throwMatching(function (e) {
  expect(e).to.be.an.instanceof(MyError);
  expect(e.message).to.equal("Error message");
  expect(e.foo).to.deep.equal(["foo"]);
});

or (?)

expect(theFunctionToTest).to.throwMatching(MyError, "Error Message", function (e) {
  // Some advanced assertions here...
  expect(e.foo).to.deep.equal(["foo"]);
});

That will become something like that for Chai as Promised:

return expect(theFunctionToTest()).to.be.rejectedWith(function (e) {
  expect(e).to.be.an.instanceof(MyError);
  expect(e.message).to.equal("Error message");
  expect(e.foo).to.deep.equal(["foo"]);
});

or (?)

return expect(theFunctionToTest()).to.be.rejectedWith(MyError, "Error Message",
  function (e) {
    // Some advanced assertions here... 
    expect(e.foo).to.deep.equal(["foo"]);
  }
);

@keithamus
Copy link
Member

Ah I think I see what you want. In which case, my comments don't really contribute here.

We have .satisfy which loosely defines a "function to test the value against arbitrary criteria". It doesn't fit the use case exactly but might be able to be bent to your will, or at least used as ground work for building similar functionality.

Also, chai's sync .to.throw pushes the error as the object to chain assertions off of - so in a sync world we can do:

expect(throwingFunction).to.throw(MyError).and.have.property('foo').deep.equal(['foo']);

Theoretically there's nothing stopping you doing the same within Chai-As-Promised, as it should convert all assertions to run in the promise chain, so something like:

expect(theFunctionToTest()).to.be.rejectedWith(MyError).and.eventually.have.property('foo').deep.equal(['foo']);

However, I'm not sure this would work as it seems rejectedWith does not alter the object flag. If it did, then I don't see why this code wouldn't work.

@domenic
Copy link
Collaborator

domenic commented Jul 1, 2015

Oooh, I did not know that about .throw. Seems like a good thing for .rejected and .rejectedWith to do.

@domenic domenic reopened this Jul 1, 2015
@keithamus
Copy link
Member

You've probably figured this; but it could well be a breaking change - as any existing code that chains off of .rejected[With] will assume flag(this, 'object') has not changed.

@valscion
Copy link

valscion commented Jul 3, 2015

chains off of .rejected[With]

Was chaining it ever supported? If it would still work with the rejectedWith('something').and.notify(done) syntax, as far as I an see there won't be any breakage.

@valscion
Copy link

valscion commented Jul 3, 2015

@yvele I think you could probably chain your custom error messages with the .rejected matcher, as it supports chaining. I tried one during refactoring old API code to use promises, and I needed the promise to be rejected with an object, not an error, during refactoring. The way I did was this:

expect(somePromise).to.eventually.be.rejected.and.deep.equal({
  data: expectedData,
  errorMessage: expectedMessage
}).notify(done);

It might work even without the notify as long as you return the result. I had to work around sinon.js stubbing setTimeout & friends which the ES6 Promise polyfill happen to use, reason why I have to stick with the done argument in test.

@domenic
Copy link
Collaborator

domenic commented Aug 5, 2016

I'm rolling up all bugs in the category "isRejected/rejectedWith doesn't behave like Chai's error rejectors" into #166.

@domenic domenic closed this as completed Aug 5, 2016
lddubeau added a commit to lddubeau/chai-as-promised that referenced this issue Sep 18, 2016
lddubeau added a commit to lddubeau/chai-as-promised that referenced this issue Sep 18, 2016
- `.fulfilled`, `.not.rejected` and `.not.rejectedWith` when successful:

  * The promise they return is fulfilled with the same value as the
    promise they were testing.

- When `.not.fulfilled`, `.rejected` and `.rejectedWith` are successful:

  * The promise they return is fulfilled with the rejection
    reason.

In all cases, when they fail, the promise they return is rejected and
has for reason the assertion that failed.

The changes above make it so that the `object` flag is automatically
set when the promise returned is resolved, and thus also allows
chaining.

This fixes chaijs#113.
domenic pushed a commit that referenced this issue Sep 27, 2016
- `.fulfilled`, `.not.rejected` and `.not.rejectedWith` when successful: the promise they return is fulfilled with the same value as the promise they were testing.
- When `.not.fulfilled`, `.rejected` and `.rejectedWith` are successful: The promise they return is fulfilled with the rejection reason.

In all cases, when they fail, the promise they return is rejected and has for reason the assertion that failed.

The changes above make it so that the `object` flag is automatically set when the promise returned is resolved, and thus also allows chaining.

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

No branches or pull requests

4 participants