Что тестировать на фронте, а что не стоит
Самый бесполезный тест, который я видел на ревью, был зелёным всегда. Разбираю, чем полезный тест отличается от теста ради процента покрытия.
Самый бесполезный тест из тех, что я видел на ревью:
it('рендерит компонент', () => {
const wrapper = mount(UserCard, { props: { user } });
expect(wrapper.exists()).toBe(true);
});
Он зелёный всегда. Он не поймает ни одного бага. Но покрытие поднимает, поэтому его и написали - у команды была цель по проценту.
Простой критерий
Хороший тест падает по одной причине: пользователь больше не может сделать то, что мог. Плохой падает при рефакторинге, когда поведение не изменилось.
Значит, тестировать надо то, что видит пользователь, а не то, как это устроено внутри:
// развалится от переименования переменной
expect(wrapper.vm.internalFilterState).toEqual({ active: true });
// переживёт любой рефакторинг
await userEvent.click(screen.getByRole('button', { name: 'Только активные' }));
expect(screen.getAllByRole('row')).toHaveLength(3);
Что писать обязательно
Чистые функции с логикой. Расчёты, форматирование, парсинг, права доступа. Самые дешёвые и самые полезные тесты вообще:
describe('calcPenalty', () => {
it('не начисляет пени в льготный период', () => {
expect(calcPenalty({ debt: 1000, daysOverdue: 10 })).toBe(0);
});
it('считает 1/300 ставки за день после 30 дней', () => {
expect(calcPenalty({ debt: 1000, daysOverdue: 31, rate: 0.15 })).toBeCloseTo(0.5);
});
});
Деньги, даты и права. Три темы, на которых я видел самые дорогие инциденты. Условий много, ошибка стоит дорого, руками все ветки не перещёлкаешь.
Каждый найденный баг. Сначала тест, который его воспроизводит, потом фикс. Тогда регрессия физически не вернётся незамеченной. Это, пожалуй, единственная практика из списка, которую я готов защищать в любом споре.
Три-пять сквозных сценариев на всё приложение, e2e: логин, создание основной сущности, оплата. Они ловят интеграционные поломки, которые юниты не видят в принципе.
Что писать не надо
Разметку и стили - expect(wrapper.find('.btn-primary').exists()) ломается от каждой правки вёрстки и ничего не гарантирует.
Чужие библиотеки. Vue уже протестирован, v-model работает.
Тривиальные обёртки, которые просто прокидывают пропсы дальше.
И вообще всё подряд ради процента. Покрытие - индикатор, а не цель. Девяносто процентов покрытия ассертами toBeTruthy() хуже, чем сорок процентов осмысленных тестов, потому что создаёт ложное спокойствие.
Пропорции, к которым я пришёл
Классическая пирамида на фронте немного перекашивается: самое ценное - интеграционные тесты компонентов с настоящими дочерними элементами и замоканной сетью. Они близки к поведению пользователя и при этом быстрые.
На продуктовом проекте ориентируюсь примерно так: 60% - юниты на логику (composables, утилиты, сторы), 30% - компонентные тесты ключевых экранов, 10% - e2e на критические пути.
Настройка
Vitest хорош тем, что живёт на конфиге Vite: те же алиасы, плагины, переменные окружения. Отдельного мира сборки для тестов не появляется, и это экономит кучу времени на поддержке:
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom',
globals: true,
coverage: { reporter: ['text', 'html'] },
},
});
Сеть мокаем на уровне HTTP, а не подменой модулей - тогда тест проверяет настоящий код запросов, а не заглушку вместо него:
server.use(
http.get('/api/users', () => HttpResponse.json({ items: [{ id: '1', name: 'Аня' }] })),
);
И запускать это надо в CI
Тесты, которые можно не запустить, не работают. У нас они идут до сборки и блокируют мерж. Нестабильный тест («иногда падает») чиним или удаляем в тот же день: красный CI, на который все привыкли забивать, хуже, чем полное отсутствие тестов.
Бизнесу это объясняется не через «качество кода», а через регламент: тест - это договорённость, записанная кодом. Пока он зелёный, система ведёт себя как условились. Ручная проверка того же сценария стоит человеко-часы на каждом релизе, автоматическая - секунды.
И главный вопрос, который стоит задавать вместо «сколько у нас покрытие»: какой последний баг доехал до прода и почему его не поймал ни один тест. Ответ обычно сразу показывает, что писать дальше.