Mocha для JavaScript (Node.js): полное руководство с примерами
1. Что это и зачем нужна
Mocha — это мощный, гибкий и широко используемый тестовый фреймворк для JavaScript, работающий в среде Node.js и в браузере. Он был создан в 2011 году разработчиком TJ Holowaychuk и с тех пор стал одним из стандартов де-факто для тестирования JavaScript-приложений. Mocha предоставляет разработчикам чистый, модульный и выразительный API для написания как простых модульных тестов, так и сложных интеграционных и асинхронных тестовых сценариев.
Основная цель Mocha — сделать процесс тестирования максимально простым и интуитивно понятным. В отличие от некоторых других фреймворков, Mocha не навязывает жёсткую структуру тестов и не включает в себя встроенную библиотеку утверждений (assertions). Вместо этого она предоставляет каркас (framework) для организации тестов (describe/it/hooks) и запуска их с различными отчётами (reporters), оставляя выбор библиотеки для проверки утверждений на усмотрение разработчика. Это делает Mocha чрезвычайно гибкой: вы можете использовать её с встроенным модулем assert, популярной библиотекой Chai (которая предоставляет стили BDD/TDD), Should.js или любой другой.
Зачем нужна Mocha? В современной разработке программного обеспечения тестирование — это не роскошь, а необходимость. Mocha позволяет автоматизировать проверку корректности работы кода, выявлять регрессии после изменений, документировать поведение функций и модулей, а также повышать общую надёжность и стабильность приложения. Благодаря поддержке асинхронного тестирования (колбэки, промисы, async/await) и возможности работы с различными библиотеками утверждений, Mocha идеально подходит для тестирования серверных приложений на Node.js, API, баз данных и любых других компонентов.
2. Установка
Mocha устанавливается через менеджер пакетов npm (Node Package Manager). Рекомендуется устанавливать её как зависимость для разработки (devDependency), так как тесты не нужны в production-окружении.
Точная команда для установки:
npm install --save-dev mocha
После установки вы можете запускать тесты, добавив скрипт в файл package.json:
{
"scripts": {
"test": "mocha"
}
}
Теперь тесты можно запускать командой:
npm test
Для глобальной установки (не рекомендуется, но возможно):
npm install -g mocha
3. Быстрый старт — минимальный рабочий пример
Создайте файл test.js со следующим содержимым:
const assert = require('assert');
// Функция, которую будем тестировать
function add(a, b) {
return a + b;
}
// Тестовый набор (suite)
describe('Функция add', function() {
// Конкретный тест (spec)
it('должна возвращать сумму двух чисел', function() {
// Утверждение (assertion)
assert.strictEqual(add(2, 3), 5);
});
it('должна работать с отрицательными числами', function() {
assert.strictEqual(add(-1, -1), -2);
});
});
Запустите тест:
npx mocha test.js
Вы должны увидеть зелёный отчёт о прохождении двух тестов.
4. Основные методы/функции/классы
Mocha предоставляет глобальные функции, которые не нужно импортировать (если они не отключены опцией --require). Ниже приведены 8 ключевых методов с сигнатурами, описанием и примерами.
4.1 describe(name, fn)
Сигнатура: describe(name: string, fn: function): void
Что делает: Создаёт тестовый набор (suite) — группу связанных тестов. Используется для логической организации тестов. Может быть вложенным.
describe('Калькулятор', function() {
describe('Сложение', function() {
it('должно складывать два числа', function() {
// тест
});
});
});
4.2 it(name, fn)
Сигнатура: it(name: string, fn: function): void
Что делает: Определяет отдельный тест (spec). Внутри функции fn размещаются утверждения и проверки.
it('должно возвращать true для пустого массива', function() {
const result = isEmpty([]);
assert.strictEqual(result, true);
});
4.3 before(fn)
Сигнатура: before(fn: function): void
Что делает: Выполняет функцию fn один раз перед запуском всех тестов в текущем наборе describe. Используется для настройки общего состояния (например, подключение к БД).
describe('База данных', function() {
let db;
before(function(done) {
db = new Database();
db.connect(done); // асинхронная инициализация
});
it('должна быть доступна', function() {
assert.ok(db.isConnected());
});
});
4.4 after(fn)
Сигнатура: after(fn: function): void
Что делает: Выполняет функцию fn один раз после завершения всех тестов в наборе. Используется для очистки ресурсов (закрытие соединений, удаление временных файлов).
after(function() {
db.close();
console.log('Соединение с БД закрыто');
});
4.5 beforeEach(fn)
Сигнатура: beforeEach(fn: function): void
Что делает: Выполняет функцию fn перед каждым тестом it в текущем наборе. Идеально подходит для сброса состояния или подготовки изолированных данных для каждого теста.
describe('Пользователь', function() {
let user;
beforeEach(function() {
user = new User({ name: 'Alice' });
});
it('должен иметь имя', function() {
assert.strictEqual(user.name, 'Alice');
});
it('должен быть активным по умолчанию', function() {
assert.strictEqual(user.isActive, true);
});
});
4.6 afterEach(fn)
Сигнатура: afterEach(fn: function): void
Что делает: Выполняет функцию fn после каждого теста. Полезно для очистки данных, созданных в beforeEach.
afterEach(function() {
user = null; // очищаем ссылку
});
4.7 it.skip(name, fn)
Сигнатура: it.skip(name: string, fn: function): void
Что делает: Пропускает (игнорирует) конкретный тест. Тест не будет выполняться, но будет отмечен в отчёте как пропущенный (pending). Полезно для временного отключения сломанных или ещё не реализованных тестов.
it.skip('сложный тест, который пока не готов', function() {
// Этот код не выполнится
assert.fail('Not implemented');
});
4.8 describe.skip(name, fn)
Сигнатура: describe.skip(name: string, fn: function): void
Что делает: Пропускает целый набор тестов. Все вложенные тесты и хуки (before, after и т.д.) игнорируются.
describe.skip('Устаревший модуль', function() {
it('тест 1', function() { /* ... */ });
it('тест 2', function() { /* ... */ });
});
5. Реальный практический пример (100-200 строк кода)
Рассмотрим пример тестирования простого REST API сервера на базе Express. Мы создадим модуль для управления списком задач (Todo) и напишем для него тесты, используя Mocha и библиотеку утверждений Chai.
Файл: src/todo.js (реализация)
class TodoList {
constructor() {
this.tasks = [];
this.nextId = 1;
}
addTask(title) {
if (!title || typeof title !== 'string') {
throw new Error('Title is required and must be a string');
}
const task = { id: this.nextId++, title, completed: false };
this.tasks.push(task);
return task;
}
getTask(id) {
const task = this.tasks.find(t => t.id === id);
if (!task) {
throw new Error(`Task with id ${id} not found`);
}
return task;
}
completeTask(id) {
const task = this.getTask(id);
task.completed = true;
return task;
}
deleteTask(id) {
const index = this.tasks.findIndex(t => t.id === id);
if (index === -1) {
throw new Error(`Task with id ${id} not found`);
}
this.tasks.splice(index, 1);
return true;
}
getAllTasks() {
return this.tasks;
}
}
module.exports = TodoList;
Файл: test/todo.test.js (тесты)
const chai = require('chai');
const expect = chai.expect;
const TodoList = require('../src/todo');
describe('TodoList', function() {
let todoList;
// Хук: создаём новый экземпляр перед каждым тестом
beforeEach(function() {
todoList = new TodoList();
});
describe('addTask()', function() {
it('должна добавлять задачу и возвращать её с id и completed=false', function() {
const task = todoList.addTask('Купить молоко');
expect(task).to.have.property('id', 1);
expect(task).to.have.property('title', 'Купить молоко');
expect(task).to.have.property('completed', false);
});
it('должна увеличивать id для каждой новой задачи', function() {
todoList.addTask('Задача 1');
const task2 = todoList.addTask('Задача 2');
expect(task2.id).to.equal(2);
});
it('должна выбрасывать ошибку при пустом заголовке', function() {
expect(() => todoList.addTask('')).to.throw('Title is required and must be a string');
});
it('должна выбрасывать ошибку при заголовке не строке', function() {
expect(() => todoList.addTask(123)).to.throw('Title is required and must be a string');
});
});
describe('getTask()', function() {
it('должна возвращать задачу по существующему id', function() {
todoList.addTask('Тестовая задача');
const task = todoList.getTask(1);
expect(task.title).to.equal('Тестовая задача');
});
it('должна выбрасывать ошибку для несуществующего id', function() {
expect(() => todoList.getTask(999)).to.throw('Task with id 999 not found');
});
});
describe('completeTask()', function() {
it('должна отмечать задачу как выполненную', function() {
todoList.addTask('Сделать домашку');
const completedTask = todoList.completeTask(1);
expect(completedTask.completed).to.be.true;
});
it('должна выбрасывать ошибку для несуществующей задачи', function() {
expect(() => todoList.completeTask(42)).to.throw('Task with id 42 not found');
});
});
describe('deleteTask()', function() {
it('должна удалять задачу и возвращать true', function() {
todoList.addTask('Временная задача');
const result = todoList.deleteTask(1);
expect(result).to.be.true;
expect(todoList.getAllTasks()).to.have.lengthOf(0);
});
it('должна выбрасывать ошибку при удалении несуществующей задачи', function() {
expect(() => todoList.deleteTask(100)).to.throw('Task with id 100 not found');
});
});
describe('getAllTasks()', function() {
it('должна возвращать пустой массив для нового списка', function() {
expect(todoList.getAllTasks()).to.be.an('array').that.is.empty;
});
it('должна возвращать все добавленные задачи', function() {
todoList.addTask('A');
todoList.addTask('B');
todoList.addTask('C');
expect(todoList.getAllTasks()).to.have.lengthOf(3);
});
});
});
Запуск тестов:
npm install --save-dev chai
npx mocha test/todo.test.js
6. Советы по производительности и типичные ошибки
Советы по производительности
- Используйте
--timeoutразумно: По умолчанию Mocha ждёт 2000 мс. Для асинхронных тестов, которые выполняются долго (например, работа с сетью), увеличьте таймаут:it('...', function(done) { this.timeout(10000); ... });. - Избегайте тяжёлых операций в
beforeEach: Если вам нужно подготовить данные для каждого теста, старайтесь делать это максимально быстро. Для дорогих операций (создание БД,