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.prototype can collide with other libraries or with your own code, and it is a common reason teams standardize on expect instead.
  • null and undefined cannot be asserted directly. should needs a real object to attach to, so null.should throws before your assertion ever runs. You have to route those cases through expect or assert.
  • 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 expect if 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 should only if you specifically want assertions to hang off the value itself, and you are comfortable with prototype extension and the null/undefined limitation.
  • Pick assert if 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.