Перейти к основному содержимому

Тестирование JavaScript — Vitest и Testing Library

Разработчику Архитектору

Тестирование JavaScript

Автотесты в JavaScript - зачем и уровни

Когда вы меняете функцию sum или кнопку в React-компоненте, легко случайно сломать то, что уже работало. Автоматический тест — это маленькая программа, которая запускает ваш код и проверяет ожидаемый результат. Вы нажимаете npm test, и за секунды видите: всё зелёное или есть конкретная ошибка.

Ручная проверка ("открыл сайт, нажал кнопку") нужна, но она медленная и забывается перед релизом. Тесты повторяют сценарий одинаково каждый раз.

Связь с остальным разделом: React, Vue, Node.js, Первая программа на Node.js, раздел тестирования.


Порядок первых тестов

  1. Unit-тест на чистую функцию (без DOM и сети).
  2. Тест компонента React/Vue с симуляцией клика (jsdom / Testing Library).
  3. Один эндпоинт через 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, запускает их и собирает отчёт.

VitestJest
Скорость в devОчень высокая (ESM, общий конфиг с Vite)На больших проектах часто медленнее
КонфигСекция test в vite.config.tsОтдельный jest.config.js
APIdescribe, 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 / awaituserEvent в новых версиях асинхронный

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.statusHTTP-код (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.clickasync у it
Supertest: EADDRINUSEВ тесте вызвали app.listen()Слушать порт только в server.js
Тест проходит, UI сломанТестировали implementation detailИскать по роли/тексту, как пользователь

Что попробовать

  1. Тест на sum с отрицательными числами и toThrow для невалидного ввода.
  2. Тест NotesList из Первая программа на React с моком fetch (vi.fn()).
  3. Подключить coverage: vitest run --coverage.

Связанные материалы


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.