Тестирование JavaScript — Vitest и Testing Library
Тестирование JavaScript
Автотесты в JavaScript - зачем и уровни
Когда вы меняете функцию sum или кнопку в React-компоненте, легко случайно сломать то, что уже работало. Автоматический тест — это маленькая программа, которая запускает ваш код и проверяет ожидаемый результат. Вы нажимаете npm test, и за секунды видите: всё зелёное или есть конкретная ошибка.
Ручная проверка ("открыл сайт, нажал кнопку") нужна, но она медленная и забывается перед релизом. Тесты повторяют сценарий одинаково каждый раз.
Связь с остальным разделом: React, Vue, Node.js, Первая программа на Node.js, раздел тестирования.
Порядок первых тестов
- Unit-тест на чистую функцию (без DOM и сети).
- Тест компонента React/Vue с симуляцией клика (jsdom / Testing Library).
- Один эндпоинт через Supertest (HTTP без браузера).
После трёх шагов уже есть базовая защита от регрессий; дальше — E2E и покрытие по приоритету риска.
Словарь — уровни тестов
| Термин | Что проверяет | Пример |
|---|---|---|
| Unit (модульный) | Одна функция или модуль изолированно | sum(2, 3) === 5 |
| Интеграционный | Несколько частей вместе | Express-роут + JSON-ответ через HTTP (без реального браузера) |
| Компонентный | React/Vue-компонент в "фейковом" DOM | Клик по кнопке → счётчик стал 1 |
| E2E (сквозной) | Весь сценарий в настоящем браузере | Playwright: открыть сайт, залогиниться, создать заметку |
Пирамида тестов — практическое правило — внизу много быстрых unit-тестов, выше — меньше интеграционных, наверху — немного медленных e2e. Vitest и Testing Library закрывают нижние и средние уровни; Playwright/Cypress — верх.
Vitest и Jest — что выбрать
Test runner (раннер) — программа, которая находит файлы *.test.js, запускает их и собирает отчёт.
| Vitest | Jest | |
|---|---|---|
| Скорость в dev | Очень высокая (ESM, общий конфиг с Vite) | На больших проектах часто медленнее |
| Конфиг | Секция test в vite.config.ts | Отдельный jest.config.js |
| API | describe, it, expect — тот же стиль, что у Jest | Классический вариант |
| Старые CRA-проекты | Миграция возможна | Часто уже настроен |
Новые проекты на Vite, Next (с Vitest) или Vue CLI 5+ чаще берут Vitest. Jest по-прежнему встречается в legacy и Create React App — идеи те же.
Анатомия теста — describe, it, expect
import { describe, it, expect } from 'vitest';
import { sum } from './sum.js';
describe('sum', () => {
it('складывает два числа', () => {
expect(sum(2, 3)).toBe(5);
});
});
Разбор:
describe('sum', ...)объединяет тесты, относящиеся к одной функции или модулю.it('складывает два числа', ...)описывает конкретный сценарий проверки поведения.expect(sum(2, 3)).toBe(5)задаёт строгое утверждение ожидаемого результата.- При падении assertion раннер показывает точное место ошибки и фактическое значение.
- Такой шаблон — базовый каркас почти любого unit-теста в Vitest.
| Конструкция | Смысл |
|---|---|
describe('sum', …) | Группа тестов для одной темы (функции sum) |
it('…', …) | Один конкретный сценарий ("должно сложить 2 и 3") |
expect(…).toBe(5) | Утверждение: если не равно 5 — тест падает с сообщением |
Имя файла sum.test.js или sum.spec.js — соглашение: раннер подхватывает такие файлы автоматически.
Чистые функции (первый unit-тест)
Чистая функция — на одни и те же аргументы всегда один результат, без обращения к сети, файлам и глобальному состоянию. Её проще всего тестировать.
mkdir sum-demo && cd sum-demo
npm init -y
npm install -D vitest
Разбор:
mkdir ... && cd ...создаёт отдельный каталог для примера и сразу переводит в него терминал.npm init -yинициализируетpackage.jsonс параметрами по умолчанию.npm install -D vitestставит тестовый раннер как dev-зависимость.- После этого в проекте можно добавлять тестовые файлы и запускать
vitest.
package.json:
{
"type": "module",
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
Разбор:
"type": "module"включает ESM-синтаксис (import/export) в Node.js-проекте.- Скрипт
"test": "vitest"запускает тесты в watch-режиме для локальной разработки. "test:run": "vitest run"выполняет одноразовый прогон, удобный для CI.- Раздел
scriptsстандартизирует команды запуска и упрощает onboarding команды.
"type": "module" — чтобы писать import/export, как в современном фронтенде.
src/sum.js:
export function sum(a, b) {
return a + b;
}
Разбор:
export functionделает функцию доступной в других модулях и тестах черезimport.- Параметры
aиbпринимают входные значения, аreturn a + bвозвращает результат сложения. - Функция чистая: результат зависит только от аргументов и не имеет побочных эффектов.
- Благодаря этому её легко и быстро тестировать разными наборами входных данных.
src/sum.test.js:
import { describe, it, expect } from 'vitest';
import { sum } from './sum.js';
describe('sum', () => {
it('adds numbers', () => {
expect(sum(2, 3)).toBe(5);
});
it('works with zero', () => {
expect(sum(0, 7)).toBe(7);
});
});
Разбор:
- Тестовый файл импортирует и раннер (
describe,it,expect), и проверяемую функцию. - Первый тест проверяет базовый сценарий сложения двух положительных чисел.
- Второй тест фиксирует пограничный случай с нулём, чтобы исключить регрессию в арифметике.
- Два коротких теста уже создают минимальный safety net для дальнейших изменений функции.
npm test # watch: перезапуск при сохранении
npm run test:run # один прогон — для CI
Разбор:
npm testобычно оставляют в watch-режиме при локальной разработке для быстрого feedback loop.npm run test:runделает один прогон и завершается, что подходит для пайплайнов CI.- Разделение режимов помогает не путать интерактивный процесс разработки и автоматическую проверку в Git.
- Одинаковые команды на локали и в CI уменьшают расхождения между средами.
Полезные матчеры Vitest (как в Jest):
| Матчер | Когда использовать |
|---|---|
toBe(5) | Примитивы: число, строка, true |
toEqual({ a: 1 }) | Объекты и массивы "по содержимому" |
toThrow() | Функция должна выбросить ошибку |
resolves / rejects | Для async и Promise |
React + Testing Library
jsdom — библиотека, которая имитирует DOM в Node. Браузер для unit-тестов компонента не нужен: render(<Counter />) создаёт дерево элементов в памяти.
React Testing Library — утилиты вокруг этого DOM. Философия — тестировать так, как действует пользователь — по тексту кнопки, роли button, label поля, а не по внутреннему className или data-testid (testid — запасной вариант).
npm create vite@latest my-app -- --template react
cd my-app
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
Разбор:
- Первая команда создаёт React-проект на Vite с готовым шаблоном.
cd my-appпереключает контекст в созданный проект.- Установка
vitestиjsdomдобавляет раннер и браузероподобную среду для тестов компонентов. - Пакеты Testing Library дают API для рендера компонентов, пользовательских действий и расширенных matcher-ов.
- В итоге проект готов к компонентным тестам без запуска реального браузера.
vite.config.js:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.js',
},
});
Разбор:
defineConfigзадаёт типобезопасную конфигурацию Vite.plugins: [react()]подключает обработку React/JSX в сборке и dev-сервере.- В секции
test.environment = 'jsdom'устанавливается среда тестирования, имитирующая DOM. setupFilesподключает общий init-файл, где регистрируют расширенные matcher-ы и глобальные настройки тестов.
src/test/setup.js:
import '@testing-library/jest-dom/vitest';
src/Counter.jsx:
import { useState } from 'react';
export function Counter() {
const [n, setN] = useState(0);
return (
<div>
<span data-testid="count">{n}</span>
<button type="button" onClick={() => setN((x) => x + 1)}>+1</button>
</div>
);
}
src/Counter.test.jsx — разбор по строкам:
Код ITЗагрузка примера кода…
Разбор:
render(<Counter />)монтирует компонент в тестовый DOM.screen.getByRole('button', { name: '+1' })ищет элемент по роли и доступному имени, как делает пользователь и скринридер.userEvent.setup()иawait user.click(...)эмулируют реальное взаимодействие с учётом асинхронности событий.- Проверки
toHaveTextContentфиксируют изменение интерфейса до и после клика. - Тест подтверждает пользовательский сценарий, а не внутреннюю реализацию компонента.
| Строка | Зачем |
|---|---|
render(<Counter />) | Смонтировать компонент в jsdom |
screen.getByRole('button', { name: '+1' }) | Кнопка с доступным именем "+1" |
userEvent.setup() + click | Реалистичный клик (события, фокус) |
async / await | userEvent в новых версиях асинхронный |
getBy* — элемент должен быть; иначе тест падает сразу. queryBy* — "может не быть"; findBy* — ждёт появления (удобно после fetch).
Тест API на Node (Supertest)
Здесь проверяется HTTP-ответ Express-приложения. Supertest шлёт запрос "внутрь" app без реального сетевого порта — быстро и стабильно.
npm install express
npm install -D vitest supertest
app.js:
import express from 'express';
export const app = express();
app.get('/health', (_req, res) => res.json({ ok: true }));
Важно экспортировать app, а listen вызывать только в server.js — иначе тест и продакшен-запуск конфликтуют.
app.test.js:
import request from 'supertest';
import { describe, it, expect } from 'vitest';
import { app } from './app.js';
describe('GET /health', () => {
it('returns ok', async () => {
const res = await request(app).get('/health');
expect(res.status).toBe(200);
expect(res.body).toEqual({ ok: true });
});
});
Разбор:
request(app).get('/health')отправляет запрос напрямую в Express-приложение без поднятия внешнего порта.awaitдожидается ответа, после чего доступныres.statusиres.body.- Проверка
toBe(200)валидирует HTTP-статус маршрута. toEqual({ ok: true })фиксирует контракт JSON-ответа API.- Такой тест быстро ловит поломки роутинга и формата ответа после рефакторинга.
| Часть | Смысл |
|---|---|
request(app).get('/health') | Виртуальный GET без curl |
res.status | HTTP-код (200, 404, …) |
res.body | Уже распарсенный JSON |
Добавьте тест на POST /notes с телом { text: '…' } — так вы закрепите Express 263.
E2E — граница этой статьи
Сценарии "открыл Chrome → ввёл логин → увидел список" — Playwright или Cypress. Они медленнее и чувствительнее к вёрстке, зато ловят ошибки стыка фронта и бэка. Упоминаются в Vue и React.
Vitest закрывает логику и компоненты; e2e — отдельный слой. Связка: много unit → несколько supertest → мало e2e.
CI — тесты на каждый push
- run: npm ci
- run: npm run test:run
Разбор:
npm ciустанавливает зависимости строго по lock-файлу, обеспечивая воспроизводимость окружения.npm run test:runзапускает тесты в non-watch режиме, подходящем для CI-задач.- Две строки формируют минимальный и надёжный шаг проверки качества на каждый push.
- При падении любого шага pipeline останавливается и предотвращает слияние нестабильного кода.
npm ci ставит версии строго из package-lock.json — воспроизводимая сборка.
Частые ошибки новичка
| Симптом | Причина | Что сделать |
|---|---|---|
document is not defined | Нет environment: 'jsdom' в Vitest | Добавить в vite.config |
toHaveTextContent is not a function | Нет @testing-library/jest-dom в setupFiles | Подключить setup |
| Тест "висит" | Забыли await у user.click | async у it |
| Supertest: EADDRINUSE | В тесте вызвали app.listen() | Слушать порт только в server.js |
| Тест проходит, UI сломан | Тестировали implementation detail | Искать по роли/тексту, как пользователь |
Что попробовать
- Тест на
sumс отрицательными числами иtoThrowдля невалидного ввода. - Тест
NotesListиз Первая программа на React с мокомfetch(vi.fn()). - Подключить coverage:
vitest run --coverage.
Связанные материалы
- Первая программа на Node.js
- Express — middleware
- Первая программа на React
- Next.js
- Экосистема JavaScript
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.