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 es 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?
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 es 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?
- Mocha uses exceptions or promise rejections to report failures. Even with synchronous assertions, if the first fails, the rest of the assertions are not run. I don't think that mocha really supports performing assertions after a failure. Is there a reason you need both assertions to execute? – Jacob Commented Dec 18, 2017 at 21:35
- yes. I need to check that both things happened for the test case to succeed. If the promise resolved as expected, but the function did not get called, the test case shouldn't pass. – ewok Commented Dec 18, 2017 at 21:36
5 Answers
Reset to default 3A 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;
});
});
});
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 pletes 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 remend 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");
});
});
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)
});
})
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);
})
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 pletely.
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!