Table of Contents#
- What is Chai-As-Promised?
- Why Use Multiple Expect Statements in One Test?
- Common Pitfalls with Asynchronous Assertions
- Setup: Installing and Configuring Chai-As-Promised
- Step-by-Step Guide: Multiple Expects with Chai-As-Promised
- Best Practices for Multiple Expect Statements
- Troubleshooting Common Issues
- References
What is Chai-As-Promised?#
Chai is a popular BDD/TDD assertion library for JavaScript, known for its readable syntax (e.g., expect(value).to.equal(5)). However, vanilla Chai struggles with promises: without special handling, assertions run before the promise resolves, leading to false positives or unhandled rejections.
Chai-As-Promised extends Chai to support promises directly. It adds the eventually modifier, which tells Chai to wait for the promise to settle (resolve or reject) before running the assertion. This makes testing async code as straightforward as testing sync code.
Why Use Multiple Expect Statements in One Test?#
A single test often needs to validate multiple aspects of a promise’s behavior. For example:
- After fetching a user, check their
id,name, andemail. - When a promise rejects, verify the error message, status code, and stack trace.
Using multiple expect statements in one test keeps related checks grouped, improving readability and reducing redundant setup (e.g., avoiding repeated API calls for separate tests).
Common Pitfalls with Asynchronous Assertions#
Before diving into solutions, let’s highlight pitfalls to avoid:
- Unhandled Rejections: Forgetting to handle promise rejections causes tests to fail cryptically.
- False Positives: Tests pass because assertions run before the promise settles (e.g., checking
undefinedinstead of the resolved value). - Overcomplicated
.then()Chains: Nested.then()calls with multiple assertions become unreadable.
Setup: Installing and Configuring Chai-As-Promised#
Prerequisites#
- Node.js (v14+ recommended).
- A test runner (we’ll use Mocha; Chai-As-Promised works with Jest, Jasmine, etc.).
Step 1: Install Dependencies#
npm install chai chai-as-promised mocha --save-dev Step 2: Configure Chai-As-Promised#
In your test file, require Chai and Chai-As-Promised, then enable the plugin:
// test/example.test.js
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
// Enable Chai-As-Promised
chai.use(chaiAsPromised);
// Get Chai's expect function
const expect = chai.expect; Step-by-Step Guide: Multiple Expects with Chai-As-Promised#
We’ll use a mock async function to demonstrate:
// mock-api.js
const getUser = async (userId) => {
if (userId === 999) {
throw { status: 404, message: 'User not found' };
}
return { id: userId, name: 'John Doe', email: '[email protected]' };
};
module.exports = { getUser }; Testing Resolved Promises#
Let’s test getUser(1) and validate the resolved user’s id, name, and email.
Approach 1: Use async/await (Recommended)#
async/await simplifies readability. Await the promise once, then run multiple synchronous assertions:
const { getUser } = require('./mock-api');
describe('getUser (resolved)', () => {
it('should return a user with id, name, and valid email', async () => {
// Await the promise once
const user = await getUser(1);
// Multiple expect statements
expect(user).to.be.an('object');
expect(user.id).to.equal(1);
expect(user.name).to.equal('John Doe');
expect(user.email).to.include('@'); // Check email format
});
}); Approach 2: Use eventually for Chained Assertions#
Chai-As-Promised’s eventually modifier lets you chain assertions directly on the promise:
it('should return a user with id, name, and valid email (chained)', () => {
// No need for async/await—return the promise!
return expect(getUser(1))
.to.eventually.be.an('object')
.and.to.have.property('id', 1)
.and.to.have.property('name', 'John Doe')
.and.to.have.property('email').that.includes('@');
}); Why return the promise? Test runners like Mocha wait for returned promises to settle, ensuring assertions run after the promise resolves.
Testing Rejected Promises#
Now test getUser(999), which rejects with an error. We’ll validate the error’s status and message.
Approach 1: async/await with try/catch#
describe('getUser (rejected)', () => {
it('should reject with 404 and "User not found"', async () => {
try {
await getUser(999);
// If no error, fail the test
expect.fail('Expected promise to reject');
} catch (err) {
// Multiple error assertions
expect(err).to.be.an('object');
expect(err.status).to.equal(404);
expect(err.message).to.equal('User not found');
}
});
}); Approach 2: to.be.rejected with Chained Assertions#
Chai-As-Promised provides to.be.rejected to test rejections cleanly:
it('should reject with 404 and "User not found" (chained)', () => {
return expect(getUser(999))
.to.be.rejected
.and.to.be.an('object')
.and.to.have.property('status', 404)
.and.to.have.property('message', 'User not found');
}); This is more concise than try/catch for rejection tests.
Best Practices#
1. Keep Tests Focused#
Test one behavior per test, even with multiple assertions. For example:
✅ Good: Test "fetch user" with assertions for id, name, email.
❌ Bad: Test "fetch user" and "update user" in one test.
2. Use Descriptive Error Messages#
Add messages to expect statements for clarity when tests fail:
expect(user.name).to.equal('John Doe', 'User name mismatch'); 3. Fail Fast#
Chai-As-Promised stops executing assertions after the first failure, preventing irrelevant errors. For example:
If user.id is 2 instead of 1, the test fails immediately—no need to check name or email.
4. Prefer async/await for Readability#
While chained eventually assertions work, async/await is often clearer for multiple unrelated checks.
Troubleshooting Common Issues#
Issue 1: Test Passes When It Should Fail#
Cause: The test doesn’t wait for the promise to settle.
Fix: Return the promise or use async/await:
// ❌ Fails silently (test finishes before promise resolves)
it('bad test', () => {
getUser(1).then(user => {
expect(user.id).to.equal(2); // Runs too late
});
});
// ✅ Good: Return the promise
it('good test', () => {
return getUser(1).then(user => {
expect(user.id).to.equal(2);
});
});
// ✅ Better: Use async/await
it('best test', async () => {
const user = await getUser(1);
expect(user.id).to.equal(2);
}); Issue 2: "Cannot read property 'eventually' of undefined"#
Cause: Chai-As-Promised not installed/enabled.
Fix: Ensure chai.use(chaiAsPromised) is called before using expect.
Issue 3: Unhandled Rejection Warnings#
Cause: Forgetting to test rejected promises.
Fix: Use to.be.rejected or try/catch to handle rejections explicitly.
References#
Conclusion#
Chai-As-Promised simplifies testing asynchronous code by letting you write multiple expect statements with confidence. By combining it with async/await or chained eventually assertions, you can validate promise behavior cleanly and efficiently. Remember to keep tests focused, use descriptive messages, and handle rejections explicitly to avoid common pitfalls.
Happy testing! ☕