22

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?

cyberwombat
  • 38,105
  • 35
  • 175
  • 251

4 Answers4

36

(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'); })
            ;
});
fearless_fool
  • 33,645
  • 23
  • 135
  • 217
14

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.

Sylvain Leroux
  • 50,096
  • 7
  • 103
  • 125
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');
  });
rrowland
  • 2,734
  • 2
  • 17
  • 32
0

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

    ...
Frank N
  • 9,625
  • 4
  • 80
  • 110