Skip to main content

JavaScript Assertion Libraries

Introduction

When writing tests for your JavaScript applications, you need a way to verify that your code behaves as expected. This is where assertion libraries come into play. An assertion is a statement that checks if a condition is true. If the assertion fails, it means your code isn't working as intended.

Assertion libraries provide a collection of assertion methods that help you validate different aspects of your code in a readable and expressive way. They form a critical part of your testing toolkit, working alongside testing frameworks to create comprehensive test suites.

In this guide, you'll learn:

  • What assertion libraries are and why they're important
  • Popular assertion libraries in the JavaScript ecosystem
  • How to use different assertion styles
  • How to implement assertions in your tests

What Are Assertion Libraries?

Assertion libraries are specialized JavaScript libraries that provide functions to verify that values in your code meet certain conditions. When an assertion fails, the library throws an error, which your testing framework catches and reports.

The primary purpose of assertion libraries is to make your tests:

  • More readable: By using expressive syntax that's close to natural language
  • More maintainable: By providing clear error messages when tests fail
  • More comprehensive: By offering a wide range of validation methods

1. Chai

Chai is one of the most popular assertion libraries in JavaScript. It's framework-agnostic and can be paired with any testing framework like Mocha, Jest, or Jasmine.

Chai offers three different assertion styles:

  • Should: Object-oriented style that extends Object.prototype
  • Expect: BDD-style that uses chain-capable assertions
  • Assert: TDD-style that provides more traditional assertions

2. Jest's Built-in Assertions

Jest comes with its own built-in assertion library called expect. It provides a rich set of matchers that help you validate different aspects of your code.

3. Node.js Assert

Node.js has a built-in assert module that provides a simple set of assertion functions. It's minimalistic but gets the job done for simple testing needs.

4. Unexpected

Unexpected is a more modern assertion library with extensible, customizable assertions.

Assertion Styles

Let's examine the three most common assertion styles:

BDD Style (Behavior-Driven Development)

BDD-style assertions are designed to read like natural language. Examples include Chai's expect and should interfaces, as well as Jest's expect.

javascript
// Chai expect style
expect(calculator.add(2, 3)).to.equal(5);

// Chai should style
calculator.add(2, 3).should.equal(5);

// Jest expect style
expect(calculator.add(2, 3)).toBe(5);

TDD Style (Test-Driven Development)

TDD-style assertions are more traditional and concise. Examples include Chai's assert interface and Node.js's built-in assert.

javascript
// Chai assert style
assert.equal(calculator.add(2, 3), 5);

// Node.js assert
assert.strictEqual(calculator.add(2, 3), 5);

Getting Started with Chai

Let's see how to use Chai in your project:

Installation

bash
npm install chai --save-dev

Basic Usage

javascript
const { expect } = require('chai');

// Function we want to test
function add(a, b) {
return a + b;
}

// Test using Chai's expect style
describe('Addition function', function() {
it('should add two numbers correctly', function() {
expect(add(2, 3)).to.equal(5);
});

it('should return a number', function() {
expect(add(2, 3)).to.be.a('number');
});
});

Common Chai Assertions

Here are some common assertions you can use with Chai's expect style:

javascript
// Equality
expect(value).to.equal(5); // Loose equality (==)
expect(value).to.eql({ name: 'John' }); // Deep equality for objects
expect(value).to.deep.equal({ name: 'John' }); // Another way for deep equality
expect(value).to.not.equal(10); // Negation

// Types
expect(value).to.be.a('string');
expect(value).to.be.an('array');
expect(value).to.be.an('object');

// Truthiness
expect(value).to.be.true;
expect(value).to.be.false;
expect(value).to.be.null;
expect(value).to.exist;

// Arrays
expect(array).to.include(item);
expect(array).to.have.length(3);

// Objects
expect(object).to.have.property('name');
expect(object).to.have.property('name', 'John');

// Throwing errors
expect(function() { throwingFunction() }).to.throw();
expect(function() { throwingFunction() }).to.throw(Error);
expect(function() { throwingFunction() }).to.throw('specific error message');

Getting Started with Jest's Expect

If you're using Jest, you already have access to its built-in expect function:

javascript
// No need to import anything, Jest provides expect globally

test('addition works', () => {
function add(a, b) {
return a + b;
}

expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(typeof add(2, 3)).toBe('number');
});

Common Jest Assertions

Here are some common assertions you can use with Jest's expect:

javascript
// Equality
expect(value).toBe(5); // Strict equality (===)
expect(value).toEqual({ name: 'John' }); // Deep equality for objects
expect(value).not.toBe(10); // Negation

// Types
expect(typeof value).toBe('string');
expect(Array.isArray(value)).toBe(true);

// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeDefined();

// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThan(10);
expect(value).toBeCloseTo(0.3); // For floating point comparison

// Arrays
expect(array).toContain(item);
expect(array).toHaveLength(3);

// Objects
expect(object).toHaveProperty('name');
expect(object).toHaveProperty('name', 'John');

// Throwing errors
expect(() => { throwingFunction() }).toThrow();
expect(() => { throwingFunction() }).toThrow(Error);
expect(() => { throwingFunction() }).toThrow('specific error message');

Real-World Example

Let's create a practical example of testing a user authentication module with assertions:

javascript
// userAuth.js - Module we're going to test
class UserAuth {
constructor() {
this.users = [];
}

register(username, password) {
if (!username || !password) {
throw new Error('Username and password are required');
}

if (password.length < 8) {
throw new Error('Password must be at least 8 characters long');
}

if (this.users.some(user => user.username === username)) {
throw new Error('Username already exists');
}

const user = { username, password };
this.users.push(user);
return user;
}

login(username, password) {
const user = this.users.find(user =>
user.username === username && user.password === password
);
return user ? true : false;
}
}

module.exports = UserAuth;

Now, let's write tests using Chai assertions:

javascript
const { expect } = require('chai');
const UserAuth = require('./userAuth');

describe('UserAuth', function() {
let auth;

beforeEach(function() {
auth = new UserAuth(); // Fresh instance for each test
});

describe('register method', function() {
it('should register a new user with valid credentials', function() {
const user = auth.register('johndoe', 'password123');

expect(user).to.be.an('object');
expect(user).to.have.property('username', 'johndoe');
expect(user).to.have.property('password', 'password123');
expect(auth.users).to.have.lengthOf(1);
});

it('should throw an error if username is missing', function() {
expect(() => auth.register(null, 'password123')).to.throw('Username and password are required');
});

it('should throw an error if password is too short', function() {
expect(() => auth.register('johndoe', 'pass')).to.throw('Password must be at least 8 characters long');
});

it('should throw an error if username already exists', function() {
auth.register('johndoe', 'password123');
expect(() => auth.register('johndoe', 'different123')).to.throw('Username already exists');
});
});

describe('login method', function() {
it('should return true for valid credentials', function() {
auth.register('johndoe', 'password123');
const result = auth.login('johndoe', 'password123');

expect(result).to.be.true;
});

it('should return false for invalid credentials', function() {
auth.register('johndoe', 'password123');
const result = auth.login('johndoe', 'wrongpassword');

expect(result).to.be.false;
});

it('should return false for non-existent user', function() {
const result = auth.login('nonexistent', 'password123');

expect(result).to.be.false;
});
});
});

The same tests using Jest's assertions would look like:

javascript
const UserAuth = require('./userAuth');

describe('UserAuth', () => {
let auth;

beforeEach(() => {
auth = new UserAuth(); // Fresh instance for each test
});

describe('register method', () => {
it('should register a new user with valid credentials', () => {
const user = auth.register('johndoe', 'password123');

expect(typeof user).toBe('object');
expect(user).toHaveProperty('username', 'johndoe');
expect(user).toHaveProperty('password', 'password123');
expect(auth.users).toHaveLength(1);
});

it('should throw an error if username is missing', () => {
expect(() => auth.register(null, 'password123')).toThrow('Username and password are required');
});

it('should throw an error if password is too short', () => {
expect(() => auth.register('johndoe', 'pass')).toThrow('Password must be at least 8 characters long');
});

it('should throw an error if username already exists', () => {
auth.register('johndoe', 'password123');
expect(() => auth.register('johndoe', 'different123')).toThrow('Username already exists');
});
});

describe('login method', () => {
it('should return true for valid credentials', () => {
auth.register('johndoe', 'password123');
const result = auth.login('johndoe', 'password123');

expect(result).toBe(true);
});

it('should return false for invalid credentials', () => {
auth.register('johndoe', 'password123');
const result = auth.login('johndoe', 'wrongpassword');

expect(result).toBe(false);
});

it('should return false for non-existent user', () => {
const result = auth.login('nonexistent', 'password123');

expect(result).toBe(false);
});
});
});

Custom Assertions

As your tests become more complex, you might find yourself repeating the same assertions. Many assertion libraries allow you to create custom assertions:

Custom Assertions in Chai

javascript
// Add a custom assertion to check if a string is a valid email
chai.use(function(chai, utils) {
chai.Assertion.addMethod('validEmail', function() {
const email = this._obj;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

this.assert(
emailRegex.test(email),
'expected #{this} to be a valid email',
'expected #{this} to not be a valid email'
);
});
});

// Now you can use it in your tests
expect('[email protected]').to.be.a.validEmail();
expect('invalid-email').to.not.be.a.validEmail();

Best Practices for Using Assertion Libraries

  1. Choose an assertion style and stick with it: Mixing different styles within the same project can lead to confusion.

  2. Be specific with your assertions: Test exactly what you need to test. For example, use .to.equal(5) instead of .to.be.truthy() when you specifically want to check for the value 5.

  3. Write readable assertions: The goal is for your tests to serve as documentation. Choose assertion styles that make your tests easy to understand.

  4. Use appropriate assertion methods: Different scenarios call for different assertion methods. For example, use deep equality assertions for comparing objects, not strict equality.

  5. Group related assertions: If you have multiple assertions for the same test case, keep them together.

Summary

Assertion libraries are an essential part of JavaScript testing, providing ways to verify that your code behaves as expected. We've covered:

  • What assertion libraries are and why they're important
  • Popular JavaScript assertion libraries like Chai and Jest's expect
  • Different assertion styles (BDD and TDD)
  • How to use assertions in real-world testing scenarios
  • Best practices for working with assertion libraries

By mastering assertion libraries, you'll be able to write more expressive, maintainable, and comprehensive tests for your JavaScript applications.

Additional Resources

Exercises

  1. Write assertions to test a function that calculates the average of an array of numbers.
  2. Create a custom assertion that checks if a date is in the future.
  3. Write tests with assertions for a function that validates email addresses.
  4. Compare how the same assertions would be written in Chai and Jest.
  5. Write a comprehensive test suite for a simple calculator object with add, subtract, multiply and divide methods.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)