Chai для JavaScript (Node.js): полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

1. Что такое Chai и зачем она нужна?

Chai — это мощная и гибкая библиотека для утверждений (assertion library) в экосистеме JavaScript, предназначенная для написания тестов. Она предоставляет разработчику широкий набор инструментов для проверки ожидаемого поведения кода. Основная цель Chai — сделать процесс написания тестов интуитивно понятным, читаемым и выразительным, независимо от того, какой стиль тестирования вы предпочитаете: классический TDD (Test-Driven Development) или более современный BDD (Behavior-Driven Development).

Зачем нужна Chai? В процессе разработки программного обеспечения критически важно убедиться, что каждая функция, модуль или компонент работают именно так, как задумано. Вместо того чтобы вручную проверять каждое значение с помощью if...else и выбрасывать исключения, Chai предлагает элегантный API. Она позволяет писать утверждения, которые выглядят как обычные английские предложения: expect(foo).to.be.a('string') или assert.equal(3, 3). Это значительно упрощает чтение тестов, их поддержку и отладку. Chai не является тестовым раннером (как Mocha или Jest), она работает в связке с ними, предоставляя слой для проверки условий.

Chai особенно популярна в среде Node.js, но также может использоваться и в браузере. Она поддерживает три основных интерфейса: expect (BDD-стиль, цепочки вызовов), should (BDD-стиль, расширение прототипов) и assert (TDD-стиль, классический). Благодаря своей модульности и огромному количеству плагинов (например, для работы с JSON, DOM, Promise), Chai является универсальным решением для тестирования практически любого JavaScript-кода.

2. Установка

Установка Chai в проект Node.js осуществляется через менеджер пакетов npm. Рекомендуется устанавливать её как зависимость для разработки (devDependency), так как библиотека не требуется в production-сборке.

npm install --save-dev chai

Если вы используете yarn:

yarn add --dev chai

После установки вы можете подключить Chai в вашем тестовом файле. Обычно это делается одним из двух способов:

// Импорт всего модуля (рекомендуется для Node.js)
const chai = require('chai');

// Или, если вы используете ES-модули (type: "module" в package.json)
import chai from 'chai';

3. Быстрый старт

Давайте напишем простейший тест, используя Chai и популярный тестовый раннер Mocha. Сначала установите Mocha: npm install --save-dev mocha.

Создайте файл test.js:

const chai = require('chai');
const expect = chai.expect; // Используем BDD-стиль

// Функция, которую будем тестировать
function add(a, b) {
    return a + b;
}

// Описываем тестовый набор (suite)
describe('Функция add', function() {
    // Конкретный тест (spec)
    it('должна правильно складывать два числа', function() {
        // Утверждение (assertion)
        expect(add(2, 3)).to.equal(5);
    });

    it('должна возвращать число', function() {
        expect(add(2, 3)).to.be.a('number');
    });
});

Запустите тест командой npx mocha test.js. Вы увидите зелёный отчёт о прохождении тестов. Этот пример демонстрирует базовую структуру: describe для группировки тестов, it для описания конкретного теста и expect для проверки результата.

4. Основные методы и функции

Chai предоставляет три основных API: expect, should и assert. Мы рассмотрим наиболее часто используемые методы на примере expect, так как он является самым популярным и гибким.

4.1. expect(value).to.equal(expected)

Сигнатура: expect(actual).to.equal(expected)

Что делает: Проверяет строгое равенство (===) между фактическим значением и ожидаемым. Это самый базовый метод для сравнения примитивов.

expect(5).to.equal(5); // Пройдёт
expect('hello').to.equal('hello'); // Пройдёт
expect(5).to.equal('5'); // Не пройдёт, так как разные типы (number vs string)

4.2. expect(value).to.be.a(type)

Сигнатура: expect(value).to.be.a(type)

Что делает: Проверяет тип значения (строку, число, массив, объект и т.д.). Использует внутренний механизм typeof и instanceof.

expect('test').to.be.a('string');
expect(123).to.be.a('number');
expect([1,2,3]).to.be.an('array'); // Обратите внимание на 'an' для гласных
expect({}).to.be.an('object');
expect(null).to.be.a('null'); // Специальный случай
expect(undefined).to.be.an('undefined');

4.3. expect(value).to.deep.equal(expected)

Сигнатура: expect(actual).to.deep.equal(expected)

Что делает: Выполняет глубокое (рекурсивное) сравнение объектов и массивов. В отличие от equal, который сравнивает ссылки, deep.equal сравнивает содержимое.

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
expect(obj1).to.deep.equal(obj2); // Пройдёт, содержимое одинаковое
expect(obj1).to.equal(obj2); // Не пройдёт, разные ссылки в памяти

const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
expect(arr1).to.deep.equal(arr2); // Пройдёт

4.4. expect(value).to.have.lengthOf(number)

Сигнатура: expect(value).to.have.lengthOf(number)

Что делает: Проверяет свойство length у строк, массивов или объектов, подобных массивам.

expect([1, 2, 3]).to.have.lengthOf(3);
expect('hello').to.have.lengthOf(5);
expect([1, 2, 3]).to.not.have.lengthOf(4); // Использование .not

4.5. expect(value).to.include(substring)

Сигнатура: expect(value).to.include(substring) или expect(array).to.include(element)

Что делает: Проверяет, содержит ли строка подстроку, или содержит ли массив определённый элемент. Для объектов используется .include({key: value}).

expect('Hello World').to.include('World'); // Строка
expect([1, 2, 3]).to.include(2); // Массив
expect({a: 1, b: 2}).to.include({a: 1}); // Объект (глубокое включение)

4.6. expect(fn).to.throw(error)

Сигнатура: expect(fn).to.throw([error])

Что делает: Проверяет, что переданная функция выбрасывает исключение. Можно проверить конкретный тип ошибки или сообщение.

function throwError() {
    throw new Error('Something went wrong');
}
expect(throwError).to.throw(); // Любая ошибка
expect(throwError).to.throw(Error); // Ошибка типа Error
expect(throwError).to.throw('Something went wrong'); // Сообщение
expect(throwError).to.throw(/wrong/); // Сообщение по регулярному выражению

4.7. expect(value).to.be.true / .false / .null / .undefined

Сигнатура: expect(value).to.be.true и аналогично для других

Что делает: Специализированные методы для проверки булевых значений и специальных типов.

expect(true).to.be.true;
expect(false).to.be.false;
expect(null).to.be.null;
expect(undefined).to.be.undefined;
expect(1).to.be.ok; // Проверка на "истинность" (truthy)
expect(0).to.not.be.ok; // Проверка на "ложность" (falsy)

4.8. expect(value).to.have.property(key, [value])

Сигнатура: expect(object).to.have.property(key, [value])

Что делает: Проверяет, что объект имеет собственное свойство (не из прототипа) с указанным ключом. Опционально можно проверить и значение свойства.

const user = { name: 'Alice', age: 30 };
expect(user).to.have.property('name'); // Только ключ
expect(user).to.have.property('name', 'Alice'); // Ключ и значение
expect(user).to.have.property('age').that.is.a('number'); // Цепочка вызовов

4.9. expect(value).to.be.above(n) / .below(n) / .within(min, max)

Сигнатура: expect(value).to.be.above(limit) и т.д.

Что делает: Числовые сравнения: больше, меньше, в диапазоне.

expect(10).to.be.above(5);
expect(2).to.be.below(5);
expect(7).to.be.within(5, 10); // Включительно

4.10. expect(value).to.match(regexp)

Сигнатура: expect(string).to.match(regexp)

Что делает: Проверяет, соответствует ли строка регулярному выражению.

expect('abc123').to.match(/^[a-z]+\d+$/);
expect('user@example.com').to.match(/^[\w.-]+@[\w.-]+\.\w+$/); // Простая проверка email

5. Реальный практический пример

Предположим, мы разрабатываем простой сервис для управления корзиной покупок. Напишем для него тесты с использованием Chai и Mocha.

Файл: cart.js (модуль, который тестируем)

class ShoppingCart {
    constructor() {
        this.items = [];
    }

    addItem(name, price, quantity = 1) {
        if (!name || typeof name !== 'string') {
            throw new Error('Invalid item name');
        }
        if (price <= 0 || typeof price !== 'number') {
            throw new Error('Invalid price');
        }
        if (quantity <= 0 || !Number.isInteger(quantity)) {
            throw new Error('Invalid quantity');
        }

        const existingItem = this.items.find(item => item.name === name);
        if (existingItem) {
            existingItem.quantity += quantity;
        } else {
            this.items.push({ name, price, quantity });
        }
    }

    removeItem(name) {
        const index = this.items.findIndex(item => item.name === name);
        if (index === -1) {
            throw new Error('Item not found');
        }
        this.items.splice(index, 1);
    }

    getTotal() {
        return this.items.reduce((total, item) => total + item.price * item.quantity, 0);
    }

    getItemCount() {
        return this.items.reduce((count, item) => count + item.quantity, 0);
    }

    clear() {
        this.items = [];
    }
}

module.exports = ShoppingCart;

Файл: test/cart.test.js (тесты)

const chai = require('chai');
const expect = chai.expect;
const ShoppingCart = require('../cart');

describe('ShoppingCart', function() {
    let cart;

    // Хук: выполняется перед каждым тестом
    beforeEach(function() {
        cart = new ShoppingCart();
    });

    describe('addItem', function() {
        it('должен добавить новый товар в корзину', function() {
            cart.addItem('Apple', 1.5, 2);
            expect(cart.items).to.have.lengthOf(1);
            expect(cart.items[0]).to.deep.equal({ name: 'Apple', price: 1.5, quantity: 2 });
        });

        it('должен увеличить количество существующего товара', function() {
            cart.addItem('Apple', 1.5, 2);
            cart.addItem('Apple', 1.5, 3);
            expect(cart.items).to.have.lengthOf(1);
            expect(cart.items[0].quantity).to.equal(5);
        });

        it('должен выбрасывать ошибку при невалидном имени', function() {
            expect(() => cart.addItem('', 10)).to.throw('Invalid item name');
            expect(() => cart.addItem(123, 10)).to.throw('Invalid item name');
        });

        it('должен выбрасывать ошибку при невалидной цене', function() {
            expect(() => cart.addItem('                    

Рекомендаций