Understanding Chai.js Language Mechanics

Titus Stone
Dec 19, 2017 · 4 min read

The built-in language included with Chai.js can sometimes be a bit challenging for those coming from an RSpec or JUnit influenced background, or for those new to programming. With its English-like syntax it can be tricky to understand exactly what is being asserted within a test. We’ll take a look at what the underlying mechanics are to help you better understand and write expressive Chai.js expectations.

The Chai.js library allows 3 syntaxes for writing expectations.

value.should.XXX 
expect(value).XXX
assert.XXX(value)

While a lot could be said about which of these styles is preferable, at Ibotta the expect syntax reigns. Expect is a function which takes a single argument, the value under test, or the parent of the value under test depending on the assertion.

All expectations against the value are either properties or methods. The expectations are added or chained to the output of the expect function. This works in exactly the same way as the builder pattern, where the return of each method or property is the object being built.

expect(valueUnderTest).foo.bar.whatever;

Chai.js includes three kinds of properties and two kinds of methods. These properties and methods can be chained together to create the “expectation chain.” Let’s have a look at them now.

Assertion Property

The most basic of keywords to go after the expect is an assertion property.

expect(‘foo’).undefined; // fails

Assertion properties apply one or more assertions against the value under test. In the above example, the string ‘foo’ is expected to be undefined. It isn’t, so this test would fail. What’s important to notice is that the inclusion of .undefined is actually running a function which executes an assertion.

This behavior is possible because of the way Javascript properties work. It’s easy to think that calling obj.property is simply retrieving a value, but Object.defineProperty actually allows either a static value to be retrieved or a function to be called every time that property is accessed. It’s this second behavior in Javascript that Chai.js is using to run assertions.

Chai.js includes 13 assertion properties: ok, true, false, null, undefined, NaN, exist, empty, arguments, itself, sealed, frozen, and finite.

Cosmetic Properties

Unlike matchers in RSpec, Chai.js includes cosmetic properties that have no effect on behavior, but instead add natural language to make it clearer for human readers of the code.

expect(‘foo’).to.be.undefined; // fails

Expanding on the example before, the addition of to and be doesn’t modify the behavior of the expectation. Previously “expect foo undefined” was a bit ambiguous because it didn’t describe the relationship between ‘foo’ and undefined. The addition of the cosmetic properties to and be make it clear what is expected of ‘foo’.

Chai includes 15 cosmetic properties: to, be, been, is, that, which, and, has, have, with, at, of, same, but, and does.

Be aware that because cosmetic properties are no-ops, an expectation chain that contains only cosmetic properties will pass.

expect(‘foo’).have.to.with.same.be; // passes

It’s doubtful a programmer would ever write such a statement on purpose. However, it might be created accidentally during a refactor or because the programmer was interrupted, and it would be overlooked because the test would pass.

This scenario is a good example of the TDD principle of making sure a test fails before it passes.

Flagging Property

A flagging property doesn’t make an assertion. Instead it sets a flag on the expectation chain that other assertions on the chain can read. The existence of a flag does not itself change anything. It’s up to the individual assertions to decide how to interpret the flag. Such interpretations can include negations or making the assertion more or less strict.

expect(‘foo’).to.not.be.undefined; // passes

Under the covers, when not is included in the expectation chain it adds a flag negate: true to the chain. Assertions after this point will then expect the opposite.

Chai includes 7 flagging properties: not, deep, nested, own, ordered, any, and all

Similar to cosmetic properties, having only a mix of flagging and cosmetic properties will not cause any assertions to run, and thus the test will never fail.

expect(‘foo’).to.not.be; // passes

Method

A method is an assertion property which takes one or more additional values. This allows the assertion(s) to be customized.

expect(‘foo’).to.equal(‘bar’); // fails

Chai includes quite a few methods: a (aliased as an), include (aliased as includes, contain, and contains), equal, eql, above, below, least, most, within, instanceof, property, ownPropertyDescriptor, lengthOf, match, string, keys, throw, respondTo, satisfy, closeTo, members, oneOf, change, increase, decrease, by, and fail.

It’s possible to mix methods and properties.

expect(‘foo’).to.equal(‘foo’).and.not.be.undefined; // passes

Flagging properties adds a flag which applies to all properties and methods which come after the flagged property in the expectation chain. This means order matters.

expect(‘foo’).to.not.be.undefined.and.equal(‘foo’) // fails

In the case above the not is applying the negate flag early on in the chain. This causes both the undefined and equal to expect that the value under test is not undefined, and not equal to the string ‘foo’.

Chainable Method

A chainable method combines many of the previous types into one keyword. Chainable methods can be used either as a method, giving it a value, or as a property, giving it no values and including it in the expectation chain.

expect([1, 2, 3).to.include(3); // passes
expect({ a: 1, b: 2 }).to.include.keys('a'); // passes

Chai includes 4 methods which can be treated as properties: a (aliased as an), include (aliased as includes, contain, and contains), length, lengthOf

Now that you’ve got a good grasp of the underlying Chai.js language mechanics, it’s time to hit the documentation to find the expectations that meet your needs and start writing some expressively awesome expectations!

Building Ibotta

Thoughts and experiences from Ibotta's engineering…