What assertion interfaces does Chai offer, and how do they differ?
Updated
Chai ships with three assertion interfaces — expect, should, and assert — and you pick one per test file (or per project) based on how much you want your assertions to read like English versus how explicitly you want them to behave. expect and should are the BDD styles; assert is the TDD style. All three run on Node and in the browser, and all three can be paired with any JavaScript testing framework.
The three interfaces at a glance
| Interface | Style | Entry point | Typical shape |
|---|---|---|---|
expect |
BDD | const { expect } = require('chai') |
expect(value).to.equal(3) |
should |
BDD | chai.should() (extends Object.prototype) |
value.should.equal(3) |
assert |
TDD | const { assert } = require('chai') |
assert.equal(value, 3) |
The BDD styles are chain-capable: you build a sentence out of language chains and end it with an assertion. The TDD style is a flat set of assertion functions with no chaining.
expect: the BDD style without side effects
expect wraps a value and exposes the full chainable language, but it does not touch any built-in prototypes. That makes it the safest default when you are unsure which style to choose.
const { expect } = require('chai');
expect([1, 2, 3]).to.have.lengthOf(3);
expect({ name: 'chai' }).to.have.property('name').that.equals('chai');
expect(() => { throw new Error('boom'); }).to.throw('boom');
What you get: readable, sentence-like assertions and a single import. What you give up: nothing structural — the only cost is that every assertion starts with expect(...) rather than reading as a bare property on the value.
should: the same language, attached to the object
should uses the identical chain vocabulary as expect, but it extends Object.prototype so assertions hang directly off the value.
const chai = require('chai');
chai.should();
[1, 2, 3].should.have.lengthOf(3);
({ name: 'chai' }).should.have.property('name', 'chai');
The trade-offs are the reason many teams avoid it:
- Prototype pollution. Extending
Object.prototypecan collide with other libraries or with your own code, and it is a common reason teams standardize onexpectinstead. nullandundefinedcannot be asserted directly.shouldneeds a real object to attach to, sonull.shouldthrows before your assertion ever runs. You have to route those cases throughexpectorassert.- Setup is required. You must call
chai.should()before the assertions run, which is easy to forget in a fresh file.
If your values are always defined objects and you like the bare-property reading, should is pleasant. Otherwise expect gives you the same language with none of these caveats.
assert: the TDD style
assert is a function-per-check interface with no chaining and no language chains. It is closer to Node's built-in assert module in feel.
const { assert } = require('chai');
assert.lengthOf([1, 2, 3], 3);
assert.property({ name: 'chai' }, 'name');
assert.throws(() => { throw new Error('boom'); }, 'boom');
What you get: explicit, predictable calls that are easy to grep and easy to read in a diff. What you give up: the expressive, sentence-like quality of the BDD styles — assert.lengthOf(list, 3) states a fact, while expect(list).to.have.lengthOf(3) reads as a sentence.
How to choose
- Pick
expectif you want the readable BDD language and want to avoid modifying built-in prototypes. This is the most common choice and the safest default. - Pick
shouldonly if you specifically want assertions to hang off the value itself, and you are comfortable with prototype extension and thenull/undefinedlimitation. - Pick
assertif your team prefers explicit function calls, minimal magic, and assertions that read the same way regardless of context.
You do not have to commit globally: the three interfaces are independent exports, so a project can use expect in most files and assert where an explicit call reads better. Mixing them within a single file is possible but usually hurts consistency more than it helps.
Beyond the core interfaces
Chai's assertion set is extensible. Plugins add assertions for new contexts such as vendor integration and object construction, and developers can write their own plugins to share with the community. That means the interface you choose is not a ceiling — you can extend whichever style you pick with domain-specific assertions.