Предыстория
Пришёл в команду с почти 4 годами коммерческой разработки за плечами. До этого долго работал соло, поэтому кайфовал от команды, выстроенных процессов и внутреннего взаимодействия.
Начал смотреть тесты в проекте и увидел системные проблемы. Не точечные баги, а повторяющиеся паттерны, из-за которых тесты не выполняли свою главную функцию — давать уверенность, что код работает правильно.
Решил не просто чинить отдельные тесты, а поменять подход целиком. Подготовил материал, собрал команду и провёл митап на полтора часа.
Три проблемы, которые я увидел
1. Снэпшоты вместо осмысленных проверок
Типичный тест в проекте выглядел так:
describe("UserProfileCards", () => { it("should render", () => { const wrapper = shallow( <Provider store={store}> <UserProfileCards items={mockedUserProfiles} /> </Provider> ) expect(wrapper).toMatchSnapshot() }) })
Что здесь не так? Снэпшот проверяет разметку, а не поведение. Любой рефакторинг (переименовал CSS-класс, поменял порядок атрибутов) ломает снэпшот, хотя компонент работает как раньше. А настоящий баг в логике снэпшот может пропустить.
2. Привязка к деталям реализации
Вот реальный пример из кодовой базы — компонент-стрелочка, которая складывается вверх или вниз:
import CollapseIcon from "./CollapseIcon" import { shallow } from "enzyme" describe("CollapseIcon", () => { it("should render", () => { const wrapper = shallow(<CollapseIcon opened={false} />) expect(wrapper).toMatchSnapshot() }) it("should react to closed", () => { const wrapper = shallow(<CollapseIcon opened={false} />) expect(wrapper.find(".collapse__icon").hasClass("collapseIcon_closed")).toEqual(true) }) it("should react to opened", () => { const wrapper = shallow(<CollapseIcon opened={true} />) expect(wrapper.find(".collapse__icon").hasClass("collapseIcon_opened")).toEqual(true) }) })

Три проблемы в трёх тестах:
Снэпшот не ловит баг в логике раскрытия/закрытия
Второй и третий тесты цепляются за CSS-классы — если переименуешь класс, тест падает. Если поломаешь саму анимацию, тест пройдёт
Это ложно отрицательные (падают при рефакторинге) и ложно положительные (не ловят баги) тесты одновременно
Ещё одна проблема — привязка к внутренним константам:
it("should render no-answers button", () => { const button = getButton(mockCommentsList[0]) const noAnswersText = `${mockedTranslation.t(`${tNamespace}no-answers`)}` expect(button.text()).toBe(noAnswersText) })
Тест импортирует tNamespace и mockedTranslation из внутренностей компонента. Поменяешь ключ локализации — тест упадёт, хотя кнопка по-прежнему показывает правильный текст.
Решение: один источник истины. Все константы выносятся в сборную константу, которую импортируют и компонент, и тест. При рефакторинге тест автоматически подхватывает изменения.
3. Нет ясной цели у тестов
«should render» — это не цель. Это констатация факта, что компонент не упал при рендере. Но что конкретно он должен делать?
Плохие описания из реального кода:
«should show tooltip»— при каких условиях? Что именно проверяем?«should render without tooltipLabel»— и что должно произойти?«should show tooltip»— дубликат первого описания
Хорошие описания строятся по принципу: «This test checks that the component...»:
"displays tooltip when isTooltipShow is true""renders without tooltip when tooltipLabel is undefined""displays correct tooltip content when isTooltipShow is true"
Решение: React Testing Library
Почему не починка Enzyme, а замена
Enzyme даёт прямой доступ к внутренностям компонента: state, instance, methods. Это провоцирует тестирование деталей реализации. React Testing Library не даёт этого делать by design.
Ключевой принцип RTL: тесты должны отражать, как пользователи взаимодействуют с приложением. Чем ближе тест к реальному использованию, тем больше уверенности он даёт.
Что даёт RTL
Пользовательский подход: взаимодействуем с компонентом так, как это делает пользователь (клик, ввод, чтение текста)
Устойчивость к рефакторингу: нет привязки к CSS-классам, именам переменных, внутреннему state
Простой API:
render,screen,fireEvent,waitForАсинхронность из коробки:
findByиwaitForдля компонентов с загрузкой данныхДоступность: тесты через
getByRole,getByLabelTextпроверяют, что интерфейс доступен

Как я провёл митап (и что бы сделал иначе)
Подготовка
Собрал проблемные тесты из нашей кодовой базы, структурировал в презентацию на 21 слайд: теория → проблемы (на наших примерах) → решение → чек-листы. Добавил мемы для разгрузки.
Что получилось
Полтора часа, формат доклада для всей команды
Прошёлся по полной программе: от пирамиды тестирования до конкретных чек-листов
Обратная связь: материал полезный
Что бы сделал иначе
Самый важный урок: получился доклад, а не диалог. Я так увлёкся материалом, что не оставил места для обсуждения. Команда слушала, но не участвовала.
Если бы проводил заново:
Две сессии по 45 минут с перерывом
После каждой части — авторизация: "что откликнулось? что не понятно?"
Меньше теории, больше совместного разбора реальных тестов
Пара-программинг: вместе переписываем один тест из кодовой базы
Чек-листы
Для написания тестов
Определена цель: что конкретно проверяю?
Пользовательская ориентация: тест с точки зрения пользователя, а не разработчика
Изоляция: тест не зависит от других тестов и внешних сервисов
Чёткое описание: читается как предложение — "проверяет, что компонент..."
Покрытие: основные сценарии + граничные условия + обработка ошибок
Для ревью тестов
Ясность цели: понятно, зачем этот тест существует?
Пользовательская перспектива: нет ли привязки к деталям реализации?
Изоляция: тест можно запустить отдельно?
Качество описаний: описания информативные?
Обратная связь: комментарии конструктивные?
Итого
Переход с Enzyme на React Testing Library — это не замена одной библиотеки на другую. Это смена мышления: от «тестирую внутренности компонента» к «тестирую то, что видит пользователь». Пока команда не понимает, зачем она пишет тесты, никакой инструмент не поможет — ни Enzyme, ни RTL, ни LLM-пайплайн для автоматической миграции.
Начать можно с малого. Один чек-лист. Одно правило: тестируем поведение, не реализацию. И один митап для команды — пусть даже он получится не идеальным. Мой получился докладом, а не диалогом. Но он запустил процесс, который оказался важнее любой презентации.
А что с миграцией?
Когда готовил эту статью, наткнулся на кейс Airbnb. В 2024 году они мигрировали 3500 файлов с Enzyme на RTL с помощью LLM-пайплайна. 75% файлов за 4 часа, 97% за неделю, весь проект за 6 недель вместо оценочных полутора лет ручной работы. Подробности
Масштабы у нас разные. У них 3500 файлов и выделенная команда. У нас компания поменьше и разработчик, который решил, что пора. Но проблема та же: новые тесты пишем на RTL, а старые на Enzyme никуда не делись.
Философию я описал выше: сначала понять, зачем тестируем и что проверяем. Но одного понимания мало. Поэтому сейчас я занимаюсь именно миграцией: мультиагентный пайплайн, LLM, автоматизация, текущие инструменты 2026 года. О проблемах, подходах и решениях расскажу в следующей статье.
