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
Popular JavaScript Assertion Libraries
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
.
// 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
.
// 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
npm install chai --save-dev
Basic Usage
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:
// 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:
// 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
:
// 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:
// 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:
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:
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
// 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
-
Choose an assertion style and stick with it: Mixing different styles within the same project can lead to confusion.
-
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. -
Write readable assertions: The goal is for your tests to serve as documentation. Choose assertion styles that make your tests easy to understand.
-
Use appropriate assertion methods: Different scenarios call for different assertion methods. For example, use deep equality assertions for comparing objects, not strict equality.
-
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
- Write assertions to test a function that calculates the average of an array of numbers.
- Create a custom assertion that checks if a date is in the future.
- Write tests with assertions for a function that validates email addresses.
- Compare how the same assertions would be written in Chai and Jest.
- 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! :)