Тестирование кода в JavaScript (Node.js): подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты написал программу для школьной столовой, которая автоматически считает стоимость обеда. Ты вводишь: «суп, котлета, компот» — программа выдаёт цену. Всё работает. Но что будет, если вместо «суп» ввести «Суп» с большой буквы? Или вообще «12345»? Или нажать кнопку «Рассчитать» до того, как выбрано блюдо?
Если ты не проверишь все эти случаи, программа может выдать неправильную цену, зависнуть или даже удалить базу данных. Чтобы этого не случилось, придумали тестирование кода.
Тестирование — это процесс, когда мы специально запускаем нашу программу в разных условиях и смотрим, работает ли она так, как задумано. Это как контрольная работа для твоего кода: мы даём ему задания и проверяем ответы.
В реальной жизни мы постоянно тестируем вещи. Когда ты покупаешь новый телефон, ты проверяешь: включается ли он, работает ли камера, заряжается ли батарея. Если бы производитель не протестировал телефон до продажи, ты мог бы получить бракованное устройство. Точно так же программисты тестируют свой код, чтобы пользователи не получили «бракованную» программу.
Важно: Тестирование — это не про то, чтобы доказать, что программа работает. Это про то, чтобы найти в ней ошибки. Чем больше ошибок ты найдёшь сам, тем меньше их найдёт пользователь.
Теория: как это работает изнутри
В мире программирования есть разные виды тестирования. Мы сосредоточимся на самом важном для начинающих — модульном тестировании (unit-тестах).
Представь, что программа — это конструктор LEGO. Каждая деталь — это функция или модуль. Модульное тестирование проверяет каждую деталь отдельно: правильно ли она соединяется, не сломана ли она.
В основе тестирования лежит простая идея: мы берём функцию, даём ей какие-то входные данные и сравниваем результат с тем, что ожидаем получить. Если результат совпадает — тест пройден. Если нет — тест упал (failed).
В JavaScript (Node.js) есть встроенный модуль assert (утверждение). Он проверяет, истинно ли наше утверждение. Например:
// Мы утверждаем, что 2 + 2 равно 4
// Если это правда — тест пройден, если нет — ошибка
const assert = require('assert');
assert.strictEqual(2 + 2, 4); // Всё хорошо, тест пройден
Но писать тесты вручную через assert неудобно. Поэтому программисты используют специальные библиотеки (фреймворки) для тестирования. Самая популярная в мире JavaScript — Jest. Она умеет:
- Находить тесты в проекте автоматически
- Показывать красивые отчёты о том, какие тесты прошли, а какие упали
- Измерять, сколько кода покрыто тестами (code coverage)
- Создавать «заглушки» (mocks) для внешних сервисов
Как устроен тест в Jest? Очень просто:
// describe — это группа тестов (набор)
// test или it — это один конкретный тест
// expect — это то, что мы проверяем
// toBe — это метод сравнения (матчер)
describe('Калькулятор', () => {
test('складывает 2 и 3', () => {
expect(2 + 3).toBe(5);
});
});
Когда ты запускаешь Jest, он проходит по всем файлам, находит тесты (обычно это файлы с расширением .test.js или .spec.js), запускает их и показывает результат.
Важно: Тесты должны быть изолированными. Это значит, что один тест не должен зависеть от другого. Если ты запустишь тесты в разном порядке, результат должен быть одинаковым.
Синтаксис в JavaScript (Node.js): все формы и варианты
Давай разберём основные конструкции, которые используются в тестировании на JavaScript. Мы будем использовать библиотеку Jest, так как она самая популярная и простая для новичков.
Установка Jest
Сначала нужно установить Jest в проект. Открой терминал в папке проекта и напиши:
npm init -y # Создаёт package.json
npm install --save-dev jest # Устанавливает Jest как зависимость для разработки
Потом в package.json замени строчку "test": "..." на:
"scripts": {
"test": "jest"
}
Теперь можно запускать тесты командой npm test.
Основные функции Jest
| Функция | Описание | Пример |
|---|---|---|
describe(название, функция) |
Группирует тесты по смыслу | describe('Калькулятор', () => { ... }) |
test(название, функция) |
Создаёт один тест | test('складывает числа', () => { ... }) |
it(название, функция) |
То же самое, что test (просто другой стиль) | it('должен вернуть 5', () => { ... }) |
expect(значение) |
Создаёт объект для проверки | expect(result) |
toBe(ожидание) |
Проверяет строгое равенство (===) | expect(2+2).toBe(4) |
toEqual(ожидание) |
Проверяет глубокое равенство (для объектов и массивов) | expect({a:1}).toEqual({a:1}) |
not |
Отрицание проверки | expect(2+2).not.toBe(5) |
Популярные матчеры (методы проверки)
| Матчер | Что проверяет | Пример |
|---|---|---|
toBe(value) |
Строгое равенство (===) | expect(1).toBe(1) |
toEqual(value) |
Глубокое равенство (для объектов) | expect({a:1}).toEqual({a:1}) |
toBeNull() |
Значение равно null | expect(null).toBeNull() |
toBeDefined() |
Значение не undefined | expect(0).toBeDefined() |
toBeTruthy() |
Значение истинно (true, 1, "строка" и т.д.) | expect(1).toBeTruthy() |
toBeFalsy() |
Значение ложно (false, 0, "", null, undefined) | expect(0).toBeFalsy() |
toContain(item) |
Массив содержит элемент | expect([1,2,3]).toContain(2) |
toThrow() |
Функция выбрасывает ошибку | expect(() => { throw new Error() }).toThrow() |
Структура тестового файла
Обычно тесты лежат в папке __tests__ или рядом с файлом, который тестируют. Имя файла заканчивается на .test.js.
// Файл: calculator.test.js
// Тестируем функции калькулятора
// Импортируем функцию, которую будем тестировать
const { sum, multiply } = require('./calculator');
// Группа тестов для функции sum
describe('Функция sum', () => {
test('складывает положительные числа', () => {
expect(sum(2, 3)).toBe(5);
});
test('складывает отрицательные числа', () => {
expect(sum(-2, -3)).toBe(-5);
});
test('складывает ноль с числом', () => {
expect(sum(0, 5)).toBe(5);
});
});
// Группа тестов для функции multiply
describe('Функция multiply', () => {
test('умножает положительные числа', () => {
expect(multiply(2, 3)).toBe(6);
});
test('умножает на ноль', () => {
expect(multiply(5, 0)).toBe(0);
});
});
Примеры кода — от простого к сложному
Пример 1: Тестирование простой функции
Начнём с самого простого — функция, которая возвращает приветствие.
// Файл: greeting.js
// Функция, которая возвращает приветствие
function greet(name) {
return `Привет, ${name}!`;
}
module.exports = greet;
// Файл: greeting.test.js
// Тесты для функции greet
const greet = require('./greeting');
describe('Функция greet', () => {
test('возвращает приветствие с именем', () => {
// Вызываем функцию с именем "Маша"
const result = greet('Маша');
// Проверяем, что результат — строка "Привет, Маша!"
expect(result).toBe('Привет, Маша!');
});
test('работает с пустой строкой', () => {
// Вызываем функцию с пустым именем
const result = greet('');
// Проверяем, что результат — "Привет, !"
expect(result).toBe('Привет, !');
});
});
Пример 2: Тестирование с несколькими случаями
Теперь протестируем функцию, которая проверяет, является ли число чётным.
// Файл: isEven.js
// Функция проверки чётности числа
function isEven(number) {
return number % 2 === 0;
}
module.exports = isEven;
// Файл: isEven.test.js
// Тесты для функции isEven
const isEven = require('./isEven');
describe('Функция isEven', () => {
test('возвращает true для чётного числа', () => {
expect(isEven(4)).toBe(true);
});
test('возвращает false для нечётного числа', () => {
expect(isEven(7)).toBe(false);
});
test('возвращает true для нуля', () => {
// 0 — чётное число
expect(isEven(0)).toBe(true);
});
test('возвращает false для отрицательного нечётного числа', () => {
expect(isEven(-3)).toBe(false);
});
});
Пример 3: Тестирование объектов
Проверим, правильно ли функция создаёт объект пользователя.
// Файл: user.js
// Функция создания пользователя
function createUser(name, age) {
return {
name: name,
age: age,
isAdult: age >= 18
};
}
module.exports = createUser;
// Файл: user.test.js
// Тесты для функции createUser
const createUser = require('./user');
describe('Функция createUser', () => {
test('создаёт взрослого пользователя', () => {
const user = createUser('Иван', 25);
// Проверяем свойства объекта
expect(user.name).toBe('Иван');
expect(user.age).toBe(25);
expect(user.isAdult).toBe(true);
});
test('создаёт несовершеннолетнего пользователя', () => {
const user = createUser('Петя', 15);
expect(user.name).toBe('Петя');
expect(user.age).toBe(15);
expect(user.isAdult).toBe(false);
});
test('создаёт пользователя ровно 18 лет', () => {
const user = createUser('Анна', 18);
// Проверяем весь объект целиком
expect(user).toEqual({
name: 'Анна',
age: 18,
isAdult: true
});
});
});
Пример 4: Тестирование массивов
Проверим функцию, которая фильтрует массив чисел, оставляя только чётные.
// Файл: filterEven.js
// Функция фильтрации чётных чисел
function filterEven(numbers) {
return numbers.filter(num => num % 2 === 0);
}
module.exports = filterEven;
// Файл: filterEven.test.js
// Тесты для функции filterEven
const filterEven = require('./filterEven');
describe('Функция filterEven', () => {
test('фильтрует массив с разными числами', () => {
const result = filterEven([1, 2, 3, 4, 5, 6]);
expect(result).toEqual([2, 4, 6]);
});
test('возвращает пустой массив, если нет чётных', () => {
const result = filterEven([1, 3, 5]);
expect(result).toEqual([]);
});
test('возвращает тот же массив, если все числа чётные', () => {
const result = filterEven([2, 4, 6]);
expect(result).toEqual([2, 4, 6]);
});
test('проверяет, что массив содержит определённое число', () => {
const result = filterEven([10, 15, 20, 25]);
expect(result).toContain(10);
expect(result).toContain(20);
});
});
Пример 5: Тестирование ошибок
Проверим, что функция правильно выбрасывает ошибку при неверных данных.
// Файл: divide.js
// Функция деления с проверкой
function divide(a, b) {
if (b === 0) {
throw new Error('На ноль делить нельзя!');
}
return a / b;
}
module.exports = divide;
// Файл: divide.test.js
// Тесты для функции divide
const divide = require('./divide');
describe('Функция divide', () => {
test('делит числа нормально', () => {
expect(divide(10, 2)).toBe(5);
});
test('выбрасывает ошибку при делении на ноль', () => {
// Оборачиваем вызов в функцию, чтобы Jest мог поймать ошибку
expect(() => {
divide(10, 0);
}).toThrow();
});
test('выбрасывает ошибку с правильным сообщением', () => {
expect(() => {
divide(10, 0);
}).toThrow('На ноль делить нельзя!');
});
test('работает с отрицательными числами', () => {
expect(divide(-10, 2)).toBe(-5);
expect(divide(10, -2)).toBe(-5);
});
});
Пример 6: Тестирование асинхронного кода
В реальных приложениях часто работают с базами данных или API. Jest умеет тестировать асинхронные функции.
// Файл: fetchData.js
// Функция, которая имитирует получение данных (асинхронная)
function fetchData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId === 1) {
resolve({ id: 1, name: 'Иван' });
} else {
reject(new Error('Пользователь не найден'));
}
}, 100);
});
}
module.exports = fetchData;
// Файл: fetchData.test.js
// Тесты для асинхронной функции
const fetchData = require('./fetchData');
describe('Функция fetchData', () => {
// Вариант 1: возвращаем Promise
test('возвращает данные пользователя с id=1', () => {
return fetchData(1).then(data => {
expect(data).toEqual({ id: 1, name: 'Иван' });
});
});
// Вариант 2: используем async/await (рекомендуется)
test('возвращает данные пользователя с id=1 (async)', async () => {
const data = await fetchData(1);
expect(data.name).toBe('Иван');
});
// Вариант 3: проверяем ошибку
test('выбрасывает ошибку для несуществующего пользователя', async () => {
await expect(fetchData(999)).rejects.toThrow('Пользователь не найден');
});
});
Пример 7: Тестирование с beforeEach и afterEach
Иногда нужно подготовить данные перед каждым тестом или очистить после. Для этого есть специальные функции.
// Файл: shoppingCart.js
// Простая корзина покупок
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
}
removeItem(itemName) {
this.items = this.items.filter(item => item.name !== itemName);
}
getTotal() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
getCount() {
return this.items.length;
}
}
module.exports = ShoppingCart;
// Файл: shoppingCart.test.js
// Тесты для корзины покупок
const ShoppingCart = require('./shoppingCart');
describe('Корзина покупок', () => {
let cart; // Переменная будет доступна во всех тестах
// Выполняется перед КАЖДЫМ тестом
beforeEach(() => {
cart = new ShoppingCart(); // Создаём новую корзину
cart.addItem({ name: 'Яблоко', price: 50 });
cart.addItem({ name: 'Молоко', price: 80 });
});
// Выполняется после КАЖДОГО теста
afterEach(() => {
cart = null; // Очищаем корзину
});
test('добавляет товары в корзину', () => {
expect(cart.getCount()).toBe(2);
});
test('удаляет товар из корзины', () => {
cart.removeItem('Яблоко');
expect(cart.getCount()).toBe(1);
expect(cart.getTotal()).toBe(80);
});
test('считает общую сумму', () => {
expect(cart.getTotal()).toBe(130); // 50 + 80
});
test('пустая корзина имеет сумму 0', () => {
const emptyCart = new ShoppingCart();
expect(emptyCart.getTotal()).toBe(0);
expect(emptyCart.getCount()).toBe(0);
});
});
Сравнение с Python: чем похоже и чем отличается
Если ты уже знаком с Python, тебе будет легче понять тестирование в JavaScript. Давай сравним.
| Характеристика | JavaScript (Jest) | Python (unittest / pytest) |
|---|---|---|
| Основная библиотека | Jest (устанавливается отдельно) | unittest (встроен) или pytest |
| Группировка тестов | describe('название', () => {}) |
Класс, наследующий unittest.TestCase |
| Отдельный тест | test('название', () => {}) |
Метод класса, начинающийся с test_ |
| Проверка равенства | expect(a).toBe(b) |
self.assertEqual(a, b) |
| Проверка истинности | expect(a).toBeTruthy() |
self.assertTrue(a) |
| Проверка ошибок | expect(() => {...}).toThrow() |
self.assertRaises(Error, func) |
| Подготовка данных | beforeEach(() => {}) |
def setUp(self): |
| Запуск тестов | npm test |
python -m unittest |
Похоже:
- И там, и там тесты изолированы друг от друга
- Есть функции для подготовки и очистки данных
- Можно проверять ошибки и исключения
- Тесты можно запускать автоматически
Отличия:
- В JavaScript тесты — это обычные функции, а в Python — методы классов
- Jest использует более «человечный» синтаксис:
expect(...).toBe(...)читается как «ожидаем, что ... равно ...» - В Python нужно помнить, что методы должны начинаться с
test_, иначе они не запустятся - Jest автоматически находит тесты, а в Python нужно указывать, какие файлы запускать
Совет: Если ты знаешь Python, начни с Jest — он проще и интуитивнее. Если знаешь JavaScript, тебе будет легко освоить pytest в Python.
Типичные ошибки и подводные камни
Даже опытные программисты иногда допускают ошибки в тестах. Вот самые распространённые.
Ошибка 1: Тест проходит, но код неправильный
Самая коварная ошибка — тест написан неправильно и всегда проходит, даже если код сломан.
// ❌ Плохой тест: он всегда проходит
test('проверка сложения', () => {
const result = sum(2, 3);
// Забыли написать expect! Тест пройден, но ничего не проверяет
});
// ✅ Хороший тест
test('проверка сложения', () => {
const result = sum(2, 3);
expect(result).toBe(5);
});
Ошибка 2: Тест зависит от других тестов
Если тесты влияют друг на друга, они могут проходить по отдельности, но падать при запуске всех вместе.
// ❌ Плохо: тесты используют одну и ту же переменную
let counter = 0;
test('первый тест', () => {
counter++;
expect(counter).toBe(1);
});
test('второй тест', () => {
// Ой! counter уже равен 1, а мы ожидаем 0
counter++;
expect(counter).toBe(1); // Упадёт!
});
// ✅ Хорошо: каждый тест создаёт свои данные
test('первый тест', () => {
let counter = 0;
counter++;
expect(counter).toBe(1);
});
test('второй тест', () => {
let counter = 0;
counter++;
expect(counter).toBe(1);
});
Ошибка 3: Сравнение объектов через toBe
toBe использует строгое равенство (===). Для объектов и массивов это работает не так, как ты думаешь.
// ❌ Плохо: toBe сравнивает ссылки на объекты
test('сравнение объектов', () => {
expect({ a: 1 }).toBe({ a: 1 }); // Упадёт! Это разные объекты
});
// ✅ Хорошо: toEqual сравнивает содержимое
test('сравнение объектов', () => {
expect({ a: 1 }).toEqual({ a: 1 }); // Пройдёт
});
Ошибка 4: Забыли про асинхронность
Если функция асинхронная, а ты не используешь async/await или return, тест может пройти до того, как функция выполнится.
// ❌ Плохо: тест завершится до выполнения Promise
test('асинхронный тест', () => {
fetchData(1).then(data => {
expect(data.name).toBe('Иван'); // Этот код может не выполниться
});
// Тест завершится здесь, не дождавшись ответа
});
// ✅ Хорошо: используем async/await
test('асинхронный тест', async () => {
const data = await fetchData(1);
expect(data.name).toBe('Иван');
});
Ошибка 5: Тестирование слишком многого в одном тесте
Один тест должен проверять одну вещь. Если тест упадёт, ты сразу поймёшь, что именно сломалось.
// ❌ Плохо: один тест проверяет всё сразу
test('функция работает', () => {
expect(sum(1, 2)).toBe(3);
expect(sum(-1, -2)).toBe(-3);
expect(sum(0, 0)).toBe(0);
expect(multiply(2, 3)).toBe(6); // Это уже другая функция!
});
// ✅ Хорошо: каждый тест проверяет одно
test('сумма положительных чисел', () => {
expect(sum(1, 2)).toBe(3);
});
test('сумма отрицательных чисел', () => {
expect(sum(-1, -2)).toBe(-3);
});
test('умножение чисел', () => {
expect(multiply(2, 3)).toBe(6);
});
Лучшие практики и советы профессионалов
Вот что советуют опытные разработчики, которые пишут тесты каждый день.
1. Пиши тесты до кода (TDD — Test-Driven Development)
Это подход, при котором ты сначала пишешь тест, а потом — код, который этот тест проходит. Звучит странно, но это работает!
- Подумай, что должна делать функция
- Напиши тест, который проверяет это поведение
- Запусти тест — он упадёт (красный)
- Напиши минимальный код, чтобы тест прошёл
- Запусти тест — он пройдёт (зелёный)
- Улучши код, если нужно (рефакторинг)
Этот цикл называется «Красный — Зелёный — Рефакторинг».
2. Тестируй граничные случаи
Не проверяй только «нормальные» данные. Обязательно проверь:
- Пустые значения (null, undefined, пустая строка)
- Отрицательные числа
- Очень большие числа
- Специальные символы в строках
- Границы диапазонов (0, 18, 100)
3. Называй тесты понятно
Название теста должно объяснять, что именно проверяется. Через месяц ты должен понять, что делает тест, не заглядывая в код.
// ❌ Плохое название
test('test1', () => { ... });
// ✅ Хорошее название
test('возвращает ошибку при отрицательном возрасте', () => { ... });
test('округляет цену до двух знаков после запятой', () => { ... });
4. Используй describe для группировки
Группируй тесты по функциям или сценариям. Это делает отчёт читаемым.
describe('Функция calculateDiscount', () => {
describe('для обычных клиентов', () => {
test('сумма до 1000 — без скидки', () => { ... });
test('сумма от 1000 до 5000 — скидка 5%', () => { ... });
});
describe('для VIP-клиентов', () => {
test('сумма до 1000 — скидка 10%', () => { ... });
test('сумма от 1000 — скидка 20%', () => { ... });
});
});