0

When chai.expect assertions fail, they normally fail the test and the negative result gets added to the report for the test runner (in this case mocha).

However, when I use a generator function wrapped using co.wrap(), as seen below, something strange happens: when the assertions pass, everything runs just fine. When the assertions fail, however, the test times out.

How can co be used together with mocha+chai?


it('calls API and then verifies database contents', function(done) {
  var input = {
    id: 'foo',
    number: 123,
  };
  request
    .post('/foo')
    .send(input)
    .expect(201)
    .expect({
      id: input.id,
      number: input.number,
    })
    .end(function(err) {
      if (!!err) {
        return done(err);
      }

      // Now check that database contents are correct
      co.wrap(function *() {
        var dbFoo = yield foos.findOne({
          id: input.id,
        });
        continueTest(dbFoo);
      })();

      function continueTest(dbFoo) {
        //NOTE when these assertions fail, test times out
        expect(dbFoo).to.have.property('id').to.equal(input.id);
        expect(dbFoo).to.have.property('number').to.equal(input.number);
        done();
      }
    });
});

Solution:

The problem arose due to co.wrap() swallowing the exception thrown by expect(), not allowing it bubble up to where it needed to for mocha to find it, as pointed out by @Bergi below.

The solution was to use co() instead of co.wrap(), and add .catch() and pass that the done callback, as seen below.

      // Now check that database contents are correct
      co(function *() {
        var dbFoo = yield foos.findOne({
          id: input.id,
        });
        continueTest(dbFoo);
      }).catch(done);
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
bguiz
  • 27,371
  • 47
  • 154
  • 243
  • `co.wrap` catches exceptions in the iterator, and rejects the returned promise. Not sure how mocha expects to catch them in async callbacks. – Bergi Aug 11 '15 at 17:07
  • @Bergi You're right, it was due to the exception being swallowed. The solution was to use `co()` instead of `co.wrap()`, and then tack on a `.catch(done)` to the end of it. If you put this as an answer to the question, I'll award you the correct answer, for pointing me in the right direction. – bguiz Aug 12 '15 at 00:12
  • Have you considered using somethink like [co-mocha](https://www.npmjs.com/package/co-mocha)? It is more dry to not have to write co() function every time... – David Novák Aug 21 '16 at 07:22

2 Answers2

2

co.wrap catches exceptions from the generator, and rejects the returned promise. It "swallows" the error that is thrown from the assertions in continueTest. Btw, instead of using .wrap and immediately calling it, you can just call co(…).

co(function*() {
    …
}).then(done, done); // fulfills with undefined or rejects with error

or

co(function*() {
    …
    done();
}).catch(done);

Btw, to use co properly you'd put all your asynchronous functions in a single generator:

it('calls API and then verifies database contents', function(done) {
  co(function*() {
    var input = {
      id: 'foo',
      number: 123,
    };
    yield request
      .post('/foo')
      .send(input)
      .expect(201)
      .expect({
        id: input.id,
        number: input.number,
      })
      .endAsync(); // assuming you've promisified it

    // Now check that database contents are correct
    var dbFoo = yield foos.findOne({
      id: input.id,
    });

    expect(dbFoo).to.have.property('id').to.equal(input.id);
    expect(dbFoo).to.have.property('number').to.equal(input.number);

  }).then(done, done);
});
bguiz
  • 27,371
  • 47
  • 154
  • 243
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
1

The root issue with your code here is that you're trying to yield in suptertest's end CPS callback; since this function is not a generator function yield can not be used and your exceptions will disappear into the ether, as you've seen.

Using co.wrap directly is the proper way (when using co) to give mocha a promise that it can use to track the success or failure of a test that uses generator functions and yield for async flow control, you just need to serialize your test so that the db check runs after supertest.

Your solution solves this by using co to convert the generator function to a promise, and then uses that promise to "convert" back to mocha's CPS style async by using its catch function to call done if the db check throws:

co(function *() {
    var dbFoo = yield foos.findOne({
      id: input.id,
    });
    continueTest(dbFoo);
}).catch(done);

Promises to the rescue

The good news is that supertest also supports promises and this can be done much more simply.

The important part (like shown in Bergi's answer) that you're missing is that promises, generator functions, and soon async/await, can work together. In this case we can take advantage of this to directly yield a promise, supertest's promise, inside the generator function.

This keeps the db check directly in that test generator function where any exceptions will be handled properly by co.wrap and passed as a rejection to mocha.

The test is now neatly serialized without any CPS detritus. And isn't that what the promise of these new async features in js are really about?


it('calls API and then verifies database contents', co.wrap(function*() {
  var input = {
    id: 'foo',
    number: 123,
  };

  yield request
    .post('/foo')
    .send(input)
    .expect(201)
    .expect({
      id: input.id,
      number: input.number,
    });

  // Now check that database contents are correct
  var dbFoo = yield foos.findOne({
    id: input.id,
  });

  expect(dbFoo).to.have.property('id').to.equal(input.id);
  expect(dbFoo).to.have.property('number').to.equal(input.number);  
}));

joshperry
  • 41,167
  • 16
  • 88
  • 103
  • As an aside, it is usually a best practice to adhere to the single responsibility rule when writing tests. An easy red flag to watch for is when words like "and", or "then" are used in the test description. Here I would recommend removing the supertest `expect`s and move them into a test of their own. – joshperry Apr 09 '17 at 15:04