4

I'm testing this function:

UserController.prototype.getDashboard = function getDashboard(req, res, next) {
  let pages, user;

  return this.User.findById(req.user.id)
    .populate('club')
    .execAsync()
    .then(dbUser => {
      user = dbUser;
      FB.setAccessToken(user.token);

      return FB.getAsync('me/accounts')
    })
    .then(fbPages => {
      pages = fbPages.data;

      return Promise.all(pages.map(page => {
        return FB.getAsync(`${page.id}/events`)
          .then(events => events)
          .catch(e => next(Boom.wrap(e)));
      }))
    })
    .then(events => {
      let entity = {
        user,
        pages: _.map(pages, (page, i) => _.assign({}, page, { events: events[i].data }))
      };

      return res.send(entity);
    })
    .catch(err => next(Boom.wrap(err)));
};

But when I test it, even though it passes, I get Unhandled rejection Error

This is my test:

 it('handles errors', (done) => {
  let mockError = new Error('test-error');
  let mockRequest = { user: { id: mockUser._id }};
  let mockNext = td.function();
  let capture = td.matchers.captor();

  td.replace(FB, 'getAsync');
  td.when(FB.getAsync('me/accounts'))
    .thenReturn(Promise.resolve(usersTestData.getDashboard.pages));

  td.when(FB.getAsync(`1769754093273789/events`))
    .thenReturn(Promise.resolve(usersTestData.getDashboard.events[0]))

  // Promise rejection was handled asynchronously
  td.when(FB.getAsync(`731033223716085/events`))
    .thenReturn(Promise.reject(mockError))

  td.when(mockNext(Boom.wrap(mockError)))
    .thenDo(() => { done() });

  controller.getDashboard(mockRequest, _.noop, mockNext);
});

The weird part, is that it passes. So the mockNext is being called with what I expected but I get a log in the console with the unhandled rejection. I've tried to handle .catch for each value of the mapping, but it still shows the same error (warning?).

I'm using Bluebird promises, mongoose(promisified) and testdouble. BTW this is the log from the console afte running the test:

dashBoard
Unhandled rejection Error: test-error
✓ handles errors
Sergio Prada
  • 161
  • 1
  • 10
  • Are you sure that you want to call `next` multiple times? – Bergi Dec 20 '16 at 01:25
  • Btw, have a look at [How do I access previous promise results in a `.then()` chain?](http://stackoverflow.com/q/28250680/1048572) on how to avoid those `pages` and `user` variables – Bergi Dec 20 '16 at 01:26
  • Originally it didn't have that stuff there. The map was just `Promise.all(pages.map(page => FB.getAsync(`${page.id}/events`)))` – Sergio Prada Dec 20 '16 at 01:27
  • Yes, that looks a lot more reasonable. Did it work? – Bergi Dec 20 '16 at 01:30
  • No, that is what I had before and it has the same behavior. I added that to this question to be explicit. – Sergio Prada Dec 20 '16 at 02:08
  • Just curious, what is `td` (and especially how does `td.when` work)? – Bergi Dec 21 '16 at 19:21
  • `td` is testdoublejs a minimalistic library for testing. `td.when` is to provide a mock behaviour. You should check it out. `td.when(duck('hi')).thenReturn('duck says hi')` This is mocking the function `duck` and when it is invoked with `'hi'` it will return what you specify. If `duck` however is called with different arguments it will return `undefined`. – Sergio Prada Dec 21 '16 at 19:26
  • Ah, I had missed the `td.replace(FB, "getAsync")` which provides them access to the method. Otherwise [this looks really weird](https://github.com/testdouble/testdouble.js/blob/master/docs/5-stubbing-results.md#tdwhen) :-) – Bergi Dec 21 '16 at 19:35

2 Answers2

2

After a lot of trial and error I found what was going on. The problem had to do with testdouble API .thenReturn().

The error appeared if .thenReturn(Promise.reject(...)) I configured td to use Bluebird promises and used their API for promises .thenReject(...). The result test code would be:

td.config({ promiseConstructor: require('bluebird') });

it('handles errors', (done) => {
  let mockError = new Error('test-error');
  let mockRequest = { user: { id: mockUser._id }};
  let mockNext = td.function();
  let capture = td.matchers.captor();

  td.replace(FB, 'getAsync');
  td.when(FB.getAsync('me/accounts'))
    .thenReturn(Promise.resolve(usersTestData.getDashboard.pages));

  td.when(FB.getAsync(`1769754093273789/events`))
    .thenResolve(usersTestData.getDashboard.events[0])

  td.when(FB.getAsync(`731033223716085/events`))
    .thenReject(mockError)

  td.when(mockNext(Boom.wrap(mockError)))
    .thenDo(() => { done() });

  controller.getDashboard(mockRequest, _.noop, mockNext);
});

This test would work with no warnings.

Sergio Prada
  • 161
  • 1
  • 10
1

I guess the problem is with

td.when(FB.getAsync(`731033223716085/events`))
.thenReturn(Promise.reject(mockError))

in case the getAsync function is never called and the rejected promise is never used (and no error handler is chained to it). You can tell it to ignore this by adding a handler yourself however:

function ignore(e) {}
function ignoredRejection(e) {
    var p = Promise.reject(e);
    p.catch(ignore);
    return p;
}

td.when(FB.getAsync(`731033223716085/events`))
.thenReturn(ignoredRejection(mockError))
Bergi
  • 630,263
  • 148
  • 957
  • 1,375