Enzyme мёртв. Адаптера для React 18 нет и не будет. Я стоял перед фронтенд-приложением с примерно 600 тестами на Enzyme и двумя вариантами: месяцы ручной работы или эксперимент с Claude Code, который мог провалиться на первой же сотне файлов.
Я выбрал второй вариант. И вот что из этого вышло.
Почему Enzyme пора хоронить (и почему это больно)
Enzyme создан Airbnb в 2015 году. Последний официальный адаптер - React 16. Для React 17 существовал неофициальный адаптер от Wojciech Maj, и автор этого адаптера сам призвал от Enzyme отказаться. Для React 18 и 19 - ничего. Concurrent rendering, Suspense, хуки - всё это Enzyme не поддерживает и не будет поддерживать.
При этом Enzyme до сих пор скачивают полтора миллиона раз в неделю на npm. Масштаб проблемы гигантский. Компании годами откладывают миграцию, потому что “работает же”. Работает. Пока не обновишь React.
Проблема не только техническая. Enzyme и RTL - это две разных философии тестирования. Enzyme позволяет лезть внутрь компонента: читать state, дёргать instance, искать дочерние компоненты по имени класса. RTL говорит: забудь про внутренности, тестируй то, что видит пользователь.
| Enzyme | RTL | |
|---|---|---|
| Философия | Тестирование реализации | Тестирование поведения |
| Рендеринг | shallow(), mount(), render() | Только render() - полный DOM |
| Доступ к элементам | jQuery-подобные: find(), children() | Семантические: getByRole(), getByText() |
| Внутренности | state(), instance(), props() | Нет доступа - только DOM |
| События | simulate(‘click’) | userEvent.click() |
Kent C. Dodds, автор RTL, сформулировал принцип: “Чем больше тесты похожи на реальное использование, тем больше уверенности они дают.” Звучит красиво. На практике это значит, что у тебя нет прямого аналога для половины Enzyme API. И каждый тест нужно не просто переписать - нужно переосмыслить.
У меня в проекте было около 600 тестов. Некоторые - простые, вида “отрендери компонент, проверь текст”. Другие - многоуровневые монстры с shallow rendering, прямым чтением state и цепочками find().children().at(2).props(). Обновление React стало неизбежным, а значит, миграция тоже.
Наивный подход: “AI, обнови всё”
Спойлер: не работает.
У Elio Capella из Filestage было 6000 тестов на Enzyme. Он попробовал дать AI задачу “обнови все тесты”. Результат - провал. AI механически заменял API-вызовы, но терял смысл тестов. Тест вроде зелёный, а проверяет уже не то, что нужно.
Я наступил на те же грабли. Первые попытки выглядели так: берёшь файл с тестами, скармливаешь Claude Code, получаешь результат. Запускаешь - половина тестов красная. Чинишь. Запускаешь - другая половина красная. Чинишь ту - ломается первая. Через пару часов такого цикла становится понятно: нужна система.
Проблемы две. Первая - AI хорошо справляется с механической заменой. find(’.button’) на getByRole(‘button’) - пожалуйста. simulate(‘click’) на userEvent.click() - легко. Но когда тест проверяет конкретное бизнес-правило через wrapper.state(), AI может переписать его так, что формально всё зелёное, а реальная проверка потеряна.
Вторая проблема - избыточный mocking. Команда Mattermost это тоже заметила на своём PR #30528: AI-генерированные тесты часто мокают слишком много. Тест проходит, но мокает половину приложения, и уверенности в коде он даёт ноль.
Архитектура конвейера: 10 фаз, субагенты, параллелизм
После нескольких неудачных попыток я сел и спроектировал pipeline. Не “дай AI файл и молись”, а нормальный конвейер с фазами, контролем и обратной связью.
Получилось 10 фаз. Каждая фаза - группа файлов, объединённых по какому-то признаку: модуль приложения, сложность, тип тестов. Внутри каждой фазы - подфазы по 3 файла. Почему три? Это баланс. Меньше - слишком медленно. Больше - теряется контроль, и если что-то ломается, непонятно где.
На каждую подфазу работали три параллельных субагента:
- implementer - пишет код миграции
- spec-reviewer - проверяет, что тесты после миграции тестируют то же самое
- code-reviewer - проверяет качество кода, ищет избыточный mocking и антипаттерны
Три субагента - не потому что я прочитал какой-то умный гайд про “мультиагентные системы”. Просто после десятка итераций стало понятно: один агент пишет код и сам же его одобряет. Нужен кто-то, кто посмотрит свежим взглядом. Да, в Claude Code субагент - это легковесный экземпляр через Task Tool, а не отдельный сервер. Но разделение ответственности работает.
К этой архитектуре я пришёл не сразу. Было примерно 156 промптов за весь процесс. Первые десятки ушли на эксперименты: какой контекст давать, как формулировать задачу, что включать в guidelines. Pipeline менялся раз за разом. Первая версия была линейной - файл за файлом, последовательно. Потом добавил параллелизм. Потом ревьюеров. Потом retry loop.
Кстати про контекст. Airbnb при миграции своих 3500 файлов обнаружил интересную вещь: “Choosing the right related files was more important than getting prompt engineering perfect.” Выбор правильных файлов для контекста оказался важнее идеального промпта. Я пришёл к тому же выводу. Когда я стал давать субагентам не только тестовый файл, но и исходный компонент, и примеры уже мигрированных тестов - качество выросло радикально.
Coverage как страховка: контроль до и после
Вот принцип, который спас мне кучу нервов: coverage не должен упасть после миграции. Кажется очевидным, но без автоматической проверки это забывается на третьем файле.
Я встроил контроль покрытия прямо в pipeline. Перед миграцией файла - замер coverage. После миграции - ещё один замер. Если coverage упал - файл уходит в retry loop: субагенту возвращаются ошибки и отчёт о покрытии, он переделывает.
Airbnb делал похожую вещь, только в другом масштабе. Их подход “sample, tune, sweep”: берёшь выборку проблемных файлов, дорабатываешь промпты на них, потом прогоняешь весь пул. У меня масштаб скромнее, но идея та же - итеративное улучшение.
Coverage ловил вещи, которые зелёные тесты пропускали. Тест может быть зелёным, потому что он мокает модуль и проверяет, что мок вызвался. Формально - passed. А coverage показывает: реальный код компонента вообще не выполнялся. Вот тебе и “проходящий тест”.
Были ситуации, когда AI переписывал тест, он проходил, но coverage определённой ветки кода исчезал. Без автоматической проверки я бы это не заметил - файлов сотни, вручную каждый не проверишь.
Паттерны, которые ломают AI (и как их чинить)
Некоторые Enzyme-паттерны конвертируются в RTL тривиально. mount() в render(), find(’.title’).text() в getByText(). Но есть паттерны, у которых нет прямого аналога, и вот с ними начинается самое интересное.
shallow() - самый болезненный. Enzyme-разработчики любили shallow rendering за изоляцию: рендеришь только сам компонент, дочерние не трогаешь. В RTL такого нет. Есть render() - и он рендерит всё дерево. Решение - мокать дочерние компоненты:
// Enzyme
const wrapper = shallow(<ParentComponent />);
expect(wrapper.find('ChildComponent')).toHaveLength(1);
// RTL
jest.mock('./ChildComponent', () => () => <div data-testid="child" />);
render(<ParentComponent />);
expect(screen.getByTestId('child')).toBeInTheDocument();
Работает, но каждый такой мок - это ещё один кусок кода, который нужно поддерживать. И AI иногда увлекается, мокая вообще всё подряд.
wrapper.state() и wrapper.instance() - прямого доступа к state в RTL нет. И не должно быть, по философии RTL. Если компонент хранит count в state - проверяй текст “Count: 5” на экране, а не значение переменной. На практике это значит, что некоторые тесты нужно переосмыслить целиком, а не просто заменить API-вызов.
wrapper.setProps() заменяется на rerender(), но семантика другая. В Enzyme ты мог обновить один prop. В RTL нужно перерендерить весь компонент с новыми пропсами.
RTL предлагает чёткий приоритет запросов: getByRole на первом месте, потом getByLabelText для форм, getByText для не-интерактивных элементов. И только в крайнем случае - getByTestId. AI часто ленится и ставит testId везде. Ревьюер-субагент ловил такое и отправлял на переделку.
В моём проекте самыми проблемными оказались большие файлы с тестами, где shallow rendering переплетался с прямым чтением state и цепочками find(). Такой файл на сотни строк - это не “замени API”, это “пойми, что тут тестируется, и перепиши с нуля”. Субагенту приходилось делать несколько итераций, и даже после этого я проверял вручную.
Числа и честная статистика
Мой проект: около 600 тестов, 10 фаз миграции, примерно 156 промптов за весь процесс. Субагенты работали параллельно, подфазы по 3 файла.
Для контекста - как это выглядит в индустрии:
| Компания | Масштаб | Автоматизация | Время |
|---|---|---|---|
| Airbnb | 3,500 файлов | 97% автоматически | 6 недель |
| Slack | 15,000+ тестов | 80% (AST + LLM) | ~6 месяцев |
| HubSpot | 76,000 тестов | Полностью ручная | 2.5 года |
| Mattermost | 60 файлов | Claude Code | Часы |
Airbnb - самый впечатляющий кейс. 97% файлов мигрировано автоматически, 75% - за первые четыре часа, остальные 22% - за четыре дня тюнинга промптов. Вручную осталось 3%. Они оценивали ручную миграцию в полтора года. Автоматизация сократила это до шести недель.
Slack пошёл гибридным путём: jscodeshift для частых паттернов (типа замены find и simulate - тысячи вхождений), а LLM подключали для сложных случаев с контекстом. Их тулзу можно найти на npm: @slack/enzyme-to-rtl-codemod.
HubSpot - контраст. 76 тысяч тестов, 2.5 года, полностью вручную. Пик - 4000 тестов в месяц, сотни репозиториев. Сами потом написали: “How we might approach this work differently in the age of AI.”
Mattermost ближе к моему масштабу - 60 файлов через Claude Code, за часы. Создали CLAUDE.md с project guidelines, разбили большой PR на мелкие для ревью.
Мой подход где-то между Mattermost и Airbnb по степени проработки pipeline. Не просто “дай Claude Code файл”, но и не промышленный конвейер на тысячи файлов. Для 600 тестов - адекватный уровень.
Что я сделал бы иначе
Несколько вещей, которые стали понятны постфактум.
Персоны субагентов - скорее театр. На Hacker News об этом писали прямо: назначение ролей вроде “ты senior engineer” или “ты QA expert” - это theater more than utility. Содержание промпта важнее роли. У меня субагенты различались не “персонами”, а задачами: один пишет код, другой проверяет спецификации, третий проверяет качество. Разделение ответственности работает. Театральные роли - нет.
Контекст решает. Это самый главный урок. Airbnb давал LLM промпты на 40-100 тысяч токенов, включая до 50 связанных файлов. Slack извлекал реальный DOM каждого тестового случая и передавал вместе с кодом. Один LLM без контекста даёт 40-60% success rate. С правильным контекстом - 80% и выше. Я бы с самого начала вкладывался в подготовку контекста, а не в шлифовку промптов.
Человеческая верификация обязательна. Даже при 97% автоматизации Airbnb оставшиеся 3% требовали ручной работы. Но дело не только в них. Автоматически проходящий тест не гарантирует, что он тестирует правильное поведение. Я бы закладывал больше времени на ручной review с самого начала, а не на этапе “почему coverage упал”.
Отказ от shallow rendering - это не потеря. Поначалу казалось, что без shallow() тесты станут тяжелее и медленнее. Отчасти это правда - полный render с моками зависимостей сложнее в настройке. Но тесты стали реалистичнее. Они ловят баги на стыке компонентов, которые shallow просто не видел. Это не компромисс, это апгрейд.
Подфазы по 3 файла - хороший размер, но можно было варьировать. Для простых файлов - тройка избыточна, можно было брать больше. Для сложных - иногда и одного файла на подфазу было много. Динамическая группировка по сложности сэкономила бы время.
Чеклист: как повторить это у себя
Если у вас есть проект на Enzyme и вы готовы мигрировать, вот последовательность действий.
Подготовка:
- Инвентаризация Enzyme-вызовов в проекте. Сколько файлов, какие паттерны используются (shallow, mount, state, instance, simulate). Можно grep-нуть по импортам из enzyme.
- Создать CLAUDE.md (или аналогичный документ) с project guidelines: структура проекта, конвенции именования, используемые библиотеки для тестов.
- Подготовить few-shot примеры: взять несколько файлов, мигрировать вручную, использовать как образцы для AI.
- Замерить baseline coverage.
Pipeline:
- Разбить файлы на фазы по модулям или сложности.
- Настроить субагентов: как минимум implementer и reviewer. Можно через Task Tool в Claude Code.
- Подфазы по 3-5 файлов - зависит от сложности.
- Retry loop: если тесты красные или coverage упал - автоматический возврат субагенту с ошибками.
- Coverage gate: сравнение покрытия до и после каждой подфазы.
Приоритет запросов RTL (заучить наизусть):
- getByRole - основной
- getByLabelText - для форм
- getByText - для текстовых элементов
- getByTestId - только в крайнем случае
Контекст для AI:
- Исходный компонент (не только тесты)
- Уже мигрированные примеры из вашего проекта
- Migration guidelines
- Импорты и зависимости
Ручная доработка:
- Закладывайте время на ревью. Каждый файл. Да, каждый.
- Особое внимание на файлы с shallow rendering и прямым доступом к state.
- Проверяйте, что тест реально тестирует бизнес-логику, а не просто “рендерится без ошибок”.
Полезные ресурсы:
- testing-library.com - официальная документация RTL
- @slack/enzyme-to-rtl-codemod - AST-codemod от Slack
- Статья Airbnb Engineering про их миграцию
- PR Mattermost #30528 - пример миграции через Claude Code
Миграция 600 тестов с Enzyme на RTL - это не задача на выходные. Но с правильно выстроенным конвейером это и не задача на полгода. Claude Code с субагентами, coverage-контролем и итеративными промптами превращает месяцы ручной работы в управляемый процесс. Не волшебный, не полностью автоматический - но управляемый. А это, пожалуй, главное.