Testing rejected promise in Mocha/Chai
Asked Answered
M

4

23

I have a class that rejects a promise:

Sync.prototype.doCall = function(verb, method, data) {
  var self = this;

  self.client = P.promisifyAll(new Client());

  var res = this.queue.then(function() {
    return self.client.callAsync(verb, method, data)
      .then(function(res) {
        return;
      })
      .catch(function(err) {    
        // This is what gets called in my test    
        return P.reject('Boo');
      });
  });

  this.queue = res.delay(this.options.throttle * 1000);
  return res;
};

Sync.prototype.sendNote = function(data) {
  var self = this;
  return self.doCall('POST', '/Invoice', {
    Invoice: data
  }).then(function(res) {
    return data;
  });
};

In my test:

return expect(s.sendNote(data)).to.eventually.be.rejectedWith('Boo');

However while the test passes it throws the error to the console.

Unhandled rejection Error: Boo ...

With non promise errors I have used bind to test to prevent the error from being thrown until Chai could wrap and test:

return expect(s.sendNote.bind(s, data)).to.eventually.be.rejectedWith('Boo');

However this does not work with this and returns:

TypeError: [Function] is not a thenable.

What is the correct way to test for this?

Mcgill answered 6/8, 2015 at 1:23 Comment(0)
T
5

You're getting the error because sendNote is being rejected and you're not catching it.

Try:

var callPromise = self.doCall('POST', '/Invoice', {
  Invoice: data
}).then(function(res) {
  return data;
});

callPromise.catch(function(reason) {
  console.info('sendNote failed with reason:', reason);
});

return callPromise;

Looks like you'll also have to move your existing catch one block out:

var res = this.queue.then(function() {
  return self.client.callAsync(verb, method, data)
    .then(function(res) {
      return;
    });
  }).catch(function(err) {    
    // This is what gets called in my test    
    return P.reject('Boo');
  });
Tubulate answered 6/8, 2015 at 1:31 Comment(6)
That doesn't seem to change anything. Still dumps error on consoleMcgill
I think it has to do with the queue. If I just do return P.reject() from the doCall function it worksMcgill
Ah. Move the catch one block out.Tubulate
Yes - I added a catch to the res.delay call and it worked. Sweet! If you'll update your answer I will mark it.Mcgill
the then block does nothing, it can be removed.Seafood
@mido22 The code could be cleaned up a lot; The question wasn't about how to clean up the rest of the code.Tubulate
G
37

(Disclaimer: This is a good question even for people that do not use Bluebird. I've posted a similar answer here; this answer will work for people that aren't using Bluebird.)

with chai-as-promised

Here's how you can use chai-as-promised to test both resolve and reject cases for a Promise:

var chai = require('chai');
var expect = chai.expect;
var chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);

...

it('resolves as promised', function() {
    return expect(Promise.resolve('woof')).to.eventually.equal('woof');
});

it('rejects as promised', function() {
    return expect(Promise.reject('caw')).to.be.rejectedWith('caw');
});

without chai-as-promised

You can accomplish the same without chai-as-promised like this:

it('resolves as promised', function() {
  return Promise.resolve("woof")
    .then(function(m) { expect(m).to.equal('woof'); })
    .catch(function(e) { throw e })  // use error thrown by test suite
           ;
});

it('rejects as promised', function() {
    return Promise.reject("caw")
        .then(function(m) { throw new Error('was not supposed to succeed'); })
        .catch(function(m) { expect(m).to.equal('caw'); })
            ;
});
Gushy answered 14/5, 2016 at 4:3 Comment(3)
Without using chai-as-promised, you could instead throw the error generated by the test suite. .catch(function(e) { throw e });Implacable
@Implacable - you are so very correct that I've incorporated your suggestion. Hope you don't mind! :)Gushy
Before using this solution, see Sylvain's answer https://mcmap.net/q/556678/-testing-rejected-promise-in-mocha-chaiGillette
T
15

I personally use that idiom:

it('rejects as promised', function() {
    return Promise.reject("caw")
        .then(
          (m) => { assert.fail('was not supposed to succeed'); }
          (m) => { /* some extra tests here */ }
        );
});

This is one of the rare cases then(onFulfilled, onRejected) (2 arguments) is legitimate to use.

If you chain .then(reject).catch(onRejected) as suggested in other answers, you end up entering in the catch handler every time since it will catch as well the rejection produced in the preceding then handler--which could cause evergreen tests if you're not careful enough to check that eventuality.

Tendon answered 5/1, 2018 at 18:51 Comment(0)
T
5

You're getting the error because sendNote is being rejected and you're not catching it.

Try:

var callPromise = self.doCall('POST', '/Invoice', {
  Invoice: data
}).then(function(res) {
  return data;
});

callPromise.catch(function(reason) {
  console.info('sendNote failed with reason:', reason);
});

return callPromise;

Looks like you'll also have to move your existing catch one block out:

var res = this.queue.then(function() {
  return self.client.callAsync(verb, method, data)
    .then(function(res) {
      return;
    });
  }).catch(function(err) {    
    // This is what gets called in my test    
    return P.reject('Boo');
  });
Tubulate answered 6/8, 2015 at 1:31 Comment(6)
That doesn't seem to change anything. Still dumps error on consoleMcgill
I think it has to do with the queue. If I just do return P.reject() from the doCall function it worksMcgill
Ah. Move the catch one block out.Tubulate
Yes - I added a catch to the res.delay call and it worked. Sweet! If you'll update your answer I will mark it.Mcgill
the then block does nothing, it can be removed.Seafood
@mido22 The code could be cleaned up a lot; The question wasn't about how to clean up the rest of the code.Tubulate
S
1

generic helper function:

import { assert } from 'chai'

const assertThrowsAsync = async(fn, expectedMessage) => {
    try {
        await fn()
    } catch (err) {
        if (expectedMessage) {
            assert.include(err.message, expectedMessage, `Function failed as expected, but could not find message snippet '${expectedMessage}'`)
        }
        return
    }
    assert.fail('function did not throw as expected')
}

calling it like so:

describe('demo', () => {

    it('negative: inacceptable path', async() => {
        await assertThrowsAsync(async() => {
            await someFuntionOfMine({}, ['/really/bad/path'])
        }, 'acceptable path')
    })

    ...
Shue answered 21/2, 2023 at 18:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.