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(' Также в библиотеке
tRPC для TypeScript: полное руководство с примерами
Chalk для JavaScript (Node.js): полное руководство с примерами
Работа с XML в Rust: библиотеки quick-xml, roxmltree и serde_xml_rs
Тестирование в TypeScript: Полное руководство по фреймворкам и инструментам