chai-as-promised: multiple expect statements in a single test

8.7k Views Asked by At

I'm using chai-as-promised to test some promises. My issue is I'm not sure how to have multiple expect statements in a single test. In order for the expect().to.be.fulfilled to work properly, I need to return it, like this:

it('test', () => {
  return expect(promise).to.be.fulfilled
}

... or to use notify, like this:

it('test', (done) => {
  expect(promise).to.be.fulfilled.notify(done)
}

The issue comes when I have another thing I need to check, such as that a certain function gets called, like this:

it('test', (done) => {
  var promise = doSomething()
  expect(sinon_function_spy.callCount).to.equal(1)
  expect(promise).to.be.fulfilled.notify(done)
})

The problem here is that, because doSomething() is asynchronous, the call to sinon_function_spy may not have occurred yet when I call that expect, making this test flaky. If I use a then, like this:

it('test', (done) => {
  var promise = doSomething()
  promise.then(() => {
    expect(sinon_function_spy.callCount).to.equal(1)
  })
  expect(promise).to.be.fulfilled.notify(done)
})

Then the test technically passes and fails as expected, but it will fail because the promise gets rejected, due to the thrown exception in the then call. Similarly, if I have a case where the promise is expected to reject:

it('test', (done) => {
  var promise = doSomething()
  promise.then(() => {
    expect(sinon_function_spy.callCount).to.equal(1)
  })
  expect(promise).to.be.rejected.notify(done)
})

Then the check on the sinon_function_spy never gets called, since the promise was rejected and doesn't call then.

How can I get both expect statements to reliably execute and return the correct values?

5

There are 5 best solutions below

1
On BEST ANSWER

In the case of wanting to assert that the Promise is fulfilled and a call was performed as expected, you don't really need that first part as an assertion. The mocha test case itself will fail if the Promise rejects as long as you are returning it:

it('test', () => {
  return doSomething()
    .then(() => {
      expect(sinon_function_spy.callCount).to.equal(1)
    });
});

If the Promise returned by doSomething() rejects, so will the test case. If the expect assertion fails, it will also fail the test case with that failed assertion. If you want to be a bit more explicit:

it('test', () => {
  return doSomething()
    .then(() => {
      expect(sinon_function_spy.callCount).to.equal(1)
    }, err => {
      expect(err).to.not.exist;
    });
});

...you can catch the error. Note that with this flavor of then with two callbacks, the assertion failing in the first callback will not reach the second callback, so it'll just be Mocha that sees the failed assertion.

Here's how you can do an expected failed Promise:

it('test', () => {
  return doSomething()
    .then(() => {
      throw new Error('Promise should not have resolved');
    }, err => {
      expect(err).to.exist;
      expect(sinon_function_spy.callCount).to.equal(1)
    });
})
1
On

If you're using mocha or jest as your test framework you can return the promise with expectations in your then() block:

it('test', () => {
   return doSomething().then( () => {
     expect(sinon_function_spy.callCount).to.equal(1);
   });
});

This test won't end until the promise successfully completes AND the expect has been run. If you're using jasmine you can use the jasmine-promises package to get the same functionality.

For the reverse case, I'd recommend creating a wrapper that reverse the polarity of the promise:

function reverse( promise ) {
   //use a single then block to avoid issues with both callbacks triggering
   return promise.then(
       () => { throw new Error("Promise should not succeed"); }
       e => e; //resolves the promise with the rejection error
   );
}

Now you can do

it('test', () => {
   return reverse( doSomethingWrong() ).then( error => {
       expect( error.message ).to.equal("Oh no");
   });
});
0
On

The best way I've found to do this is like so:

it('test', async () => {
  await expect(promise1).to.eventually.be.equal(1);
  await expect(promise2).to.eventually.be.equal(2);
})
0
On

One of the issues with using multiple assertions in a single test is that if the first assertion fails, we don't get feedback on the remaining assertions. In some cases, that may be just fine, but if your needs require that you get feedback on all assertions, then you're left with a few options.

One option is to see if you can use Jasmine and Jasmine's assertions instead of Chai. Jasmine, by default, checks and reports on all assertions in a test, and Jasmine is able to do this where Mocha and Chai cannot. Jasmine's test runner and assertion library are more tightly integrated, which is perhaps why they are able to pull this off.

If Jasmine isn't an option, then another idea is to put the test execution in the before block and then validate each assertion in separate it blocks. This results in greater feedback, but some test reporting tools may report this as N tests instead of a single test.

A third option is to use a Node.js module I built and published called multi-assert. This allows multiple Chai assertions (and perhaps some other assertion libraries) in the same test and will report on all of the failures. Here is an example that would work with asynchronous code:

const { expect } = require('chai');
const { multiAssertAsync } = require('multi-assert');

describe('Test - async/promises', () => {
    
    async function fetchData() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('{"status":"bluegreen"}');
            }, 300)
        });
    }

    it('should expect status to be yellowblue, yellowred, bluegreen, and 3 to equal 4', async () => {
        await multiAssertAsync([
            async () => expect((JSON.parse(await fetchData())).status).to.equal('yellowblue'),
            () => expect(3).to.equal(4),
            async () => expect((JSON.parse(await fetchData())).status).to.equal('bluegreen'),
            async () => expect((JSON.parse(await fetchData())).status).to.equal('yellowred')
        ]);
    });
});

After executing, we see that the first, third, and fourth assertions fail and report on the various defects in the code, providing the maximum transparency and opportunity for developers and testers to fix the code completely.

1) Test - async/promises
       should expect status to be yellowblue, yellowred, bluegreen, and 3 to equal 4:
     AssertionError: 

      MultipleAssertionError: expected 3 to equal 4
        at /Users/user123/proj/multi-assert/examples/example-async.spec.js:17:32
        at /Users/user123/proj/multi-assert/multi-assert-async.js:12:27
        at Array.map (<anonymous>)

      MultipleAssertionError: expected 'bluegreen' to equal 'yellowblue'
        at /Users/user123/proj/multi-assert/examples/example-async.spec.js:16:75
        at async /Users/user123/proj/multi-assert/multi-assert-async.js:12:21
        at async Promise.all (index 0)

      MultipleAssertionError: expected 'bluegreen' to equal 'yellowred'
        at /Users/user123/proj/multi-assert/examples/example-async.spec.js:19:75
        at async /Users/user123/proj/multi-assert/multi-assert-async.js:12:21
        at async Promise.all (index 3)

      at /Users/user123/proj/multi-assert/multi-assert-async.js:22:23

I haven't tried it using the .then syntax, as for me async/await may be more readable and easier to understand in many cases. If there is any issues with the alternative syntax, I'm happy to help. Hoping this helps!

0
On

A way to achieve multiple expects

it('should fail if no auth', () => {
    const promise = chai.request(server).get('/albums');
    return expect(promise).to.be.rejected.then(() => {
      return promise.catch(err => {
        expect(err).not.to.be.null;
        expect(err.response).to.have.status(401);
        expect(err.response).to.be.a.json;
      });
   });
});