TL;DR
  • Задача: перевести тесты React с Enzyme на RTL без потери замысла и покрытия.

  • Подход: LLM-управляемый пофайловый конвейер в виде машины состояний: Enzyme→RTL → Jest → ESLint --fix → фиксы линтера → TSC.

  • Ретраи: повтор шагов до успеха; на каждом повторе модель получает актуальный файл и логи валидации (динамические промпты).

  • Контекст: для сложных кейсов промпты 40–100k токенов (до ~50 связанных файлов, хорошие примеры RTL, исходники компонента и импортов).

  • Масштаб: ~3,5 тыс. файлов; 75% автоматически за 4 часа; 97% — после 4 дней цикла «sample → tune → sweep»; оставшиеся ~3% завершены вручную на базе автоконверсий.

  • Инженерные практики: автокомментарий со статусом шага прямо в файле; перезапуски по маскам путей и нужному шагу; сбор метрик отказов по стадиям.

  • Результат: сохранены смысл тестов и покрытие; весь проект занял ~6 недель и оказался существенно дешевле оценки ~1,5 лет ручной миграции.

  • Вывод: решают процесс и данные — LLM эффективна в связке с проверками, ретраями и насыщенным контекстом; подход масштабируется на другие миграции/рефакторинги.

Airbnb недавно завершила первую крупномасштабную миграцию кода под управлением LLM: мы обновили почти 3,5 тысячи файлов тестов React-компонентов, переведя их с Enzyme на React Testing Library (RTL). По первоначальным оценкам ручная работа заняла бы 1,5 года инженерного времени, но — используя сочетание передовых моделей и надёжной автоматизации — миграция завершилась всего за 6 недель.

В этой статье я расскажу о сложностях, с которыми мы столкнулись при переходе с Enzyme на RTL, почему LLM отлично подходят для такого рода задач и как мы спроектировали инструменты миграции, чтобы запускать LLM-управляемую миграцию в масштабах всей кодовой базы.

Предпосылки

В 2020 году Airbnb приняла React Testing Library (RTL) для разработки всех новых тестов React-компонентов — это были наши первые шаги в сторону отказа от Enzyme. Хотя Enzyme хорошо служил нам с 2015 года, он был рассчитан на более ранние версии React, и его глубокий доступ к внутренностям компонентов перестал соответствовать современным практикам тестирования в React.

Однако из-за принципиальных различий между этими фреймворками мы не могли просто заменить один другим (подробнее о различиях см. здесь). Мы также не могли просто удалить файлы Enzyme: анализ показал, что это приведёт к существенным «дырам» в кодовом покрытии. Чтобы выполнить миграцию, нам требовался автоматизированный способ рефакторинга тестовых файлов с Enzyme на RTL при сохранении замысла исходных тестов и уровня их покрытия.

Как мы это сделали

В середине 2023 года команда на хакатоне в Airbnb показала, что большие языковые модели способны всего за несколько дней успешно конвертировать сотни файлов Enzyme в RTL.

Опираясь на этот многообещающий результат, в 2024 году мы разработали масштабируемый пайплайн для миграции с использованием LLM. Мы разбили миграцию на дискретные, пофайловые шаги, которые можно распараллелить, добавили настраиваемые циклы повторных попыток (ретраи) и существенно расширили наши промпты дополнительным контекстом. Наконец, мы выполнили «широкофронтальную» настройку промптов для «длинного хвоста» сложных файлов.

  1. Шаги проверки файлов и рефакторинга

Мы начали с разбиения миграции на серию автоматизированных шагов валидации и рефакторинга. Представьте это как продакшн-пайплайн: каждый файл проходит стадии проверки, и когда проверка падает — мы подключаем LLM, чтобы её исправить.

Мы смоделировали поток как машину состояний, переводя файл в следующее состояние только после того, как пройдена валидация на предыдущем:

Шаги миграции для каждого файла

Шаг / условие

Действие LLM / результат

1) Есть ли в файле Enzyme?

Рефакторинг на React Testing Library (RTL)

2) Проходят ли тесты Jest?

Исправление тестов Jest

3) Запустить линтер с --fix

4) Всё ещё остались ошибки линтинга?

Исправление ошибок линтера

5) Есть проблемы с типами TypeScript?

Исправление типов

Итог

Файл готов ?

Этот пошаговый подход стал надёжной основой нашего конвейера автоматизации. Он позволил отслеживать прогресс, снижать долю отказов на конкретных шагах и при необходимости повторно запускать обработку файлов или отдельных стадий. Пошаговый подход также упростил параллельный запуск миграции для сотен файлов, что было критично как для быстрого перевода простых файлов, так и для постепенной «разборки» длинного хвоста более сложных случаев на поздних этапах миграции.

2. Циклы повторных попыток и динамические промпты

На ранних этапах миграции мы экспериментировали с разными стратегиями промпт-инжиниринга, чтобы повысить вероятность успешной конвертации каждого файла. Однако, опираясь на пошаговую схему, мы обнаружили, что наиболее эффективный способ улучшить результаты — это простой «силовой» подход: повторять шаги до тех пор, пока они не пройдут, либо пока не будет достигнут лимит попыток. Мы обновили шаги так, чтобы на каждой повторной попытке использовать динамические промпты: передавать LLM сообщения о результатах валидации и актуальную версию файла. Также мы написали раннер циклов, который прогонял каждый шаг до настраиваемого максимального числа попыток.

Схема цикла повторных попыток. Для заданного шага N, если в файле есть ошибки, мы повторяем валидацию и пытаемся исправить ошибки, пока не достигнем максимума попыток или ошибок больше не останется.
Схема цикла повторных попыток. Для заданного шага N, если в файле есть ошибки, мы повторяем валидацию и пытаемся исправить ошибки, пока не достигнем максимума попыток или ошибок больше не останется.

С этим простым циклом повторных попыток нам удалось успешно мигрировать большое количество тестовых файлов от простых до средней сложности: часть завершалась после нескольких ретраев, а большинство — к десятой попытке.

3. Расширение контекста

Для файлов до определённого уровня сложности одного лишь увеличения числа повторных попыток было достаточно. Однако для случаев со сложной инициализацией состояния тестов или чрезмерной косвенности мы нашли более действенный подход: включать в промпты максимально релевантный контекст.

К концу миграции объём наших промптов вырос до диапазона от 40 000 до 100 000 токенов: мы подтягивали до 50 связанных файлов, большое количество вручную подготовленных few-shot-примеров, а также примеры существующих, хорошо написанных и проходящих тестов из того же проекта.

В каждом промпте были:

  • Исходный код тестируемого компонента

  • Тестовый файл, который мы мигрировали

  • Ошибки валидации для текущего шага

  • Связанные тесты из той же директории (с сохранением командных паттернов)

  • Общие рекомендации по миграции и типовые решения

Вот как это выглядело на практике (существенно сокращено для удобства чтения):

// Пример кода показывает упрощённую версию промпта,
// который включает исходный код из связанных файлов, импорты,
// примеры, исходник самого компонента и тестовый файл для миграции.

const prompt = [
  'Convert this Enzyme test to React Testing Library:',
  `SIBLING TESTS:\n${siblingTestFilesSourceCode}`,
  `RTL EXAMPLES:\n${reactTestingLibraryExamples}`,
  `IMPORTS:\n${nearestImportSourceCode}`,
  `COMPONENT SOURCE:\n${componentFileSourceCode}`,
  `TEST TO MIGRATE:\n${testFileSourceCode}`,
].join('\n\n');

Этот подход с насыщенным контекстом оказался очень эффективным для более сложных файлов — LLM лучше понимала командные шаблоны, распространённые подходы к тестированию и общую архитектуру кодовой базы.

Отметим, что хотя на этом этапе мы действительно занимались промпт-инжинирингом, ключевым фактором успеха стало именно правильное включение релевантных файлов (поиск близких по расположению, хороших примеров из того же проекта, фильтрация зависимостей до действительно относящихся к компоненту и т. п.), а не идеальная «шлифовка» формулировок промптов.

После разработки и отладки наших миграционных скриптов с ретраями и богатым контекстом при первом массовом прогоне мы успешно мигрировали 75% целевых файлов всего за четыре часа.

4. С 75% до 97%: систематическое улучшение

Достигнуть показателя в 75% было приятно, но это всё ещё оставляло нам почти 900 файлов, не прошедших наши пошаговые критерии валидации. Чтобы разобраться с этим «длинным хвостом», нам потребовался системный способ понять, где именно застревают оставшиеся файлы, и улучшить миграционные скрипты так, чтобы адресовать эти проблемы. Мы также хотели двигаться широким фронтом, чтобы максимально быстро сокращать число оставшихся файлов, не застревая на самых сложных случаях миграции.

Для этого мы добавили в наши инструменты миграции две функции.

Во-первых, мы сделали простую систему, которая давала видимость типовых проблем, с которыми сталкивались скрипты: мы проставляли в файлах автоматически сгенерированный комментарий со статусом каждого шага миграции. Вот как выглядел этот комментарий в коде:

// MIGRATION STATUS: {"enyzme":"done","jest":{"passed":8,"failed":2,"total":10,"skipped":0,"successRate":80},"eslint":"pending","tsc":"pending",}

И во-вторых, мы добавили возможность легко перезапускать отдельные файлы или наборы по шаблонам путей, с фильтрацией по конкретному шагу, на котором они застряли:

$ llm-bulk-migration --step=fix-jest --match=project-abc/**

Используя эти две функции, мы могли быстро запускать цикл обратной связи для улучшения наших промптов и инструментария:

  1. Запустить все оставшиеся «падающие» файлы, чтобы найти общие проблемы, на которых спотыкается LLM

  2. Сделать выборку из 5–10 файлов, которые демонстрируют типовую проблем 

  3. Обновить промпты и скрипты, чтобы адресовать эту проблему

  4. Перезапустить на выборке «падающих» файлов, чтобы проверить, что исправление работает

  5. Повторить, прогнав снова все оставшиеся файлы

После четырёх дней прогона по схеме «sample, tune, sweep» мы увеличили долю завершённых файлов с 75% до 97% от общего числа, и оставалось чуть меньше 100 файлов. К этому моменту мы повторяли попытки по многим из этих «хвостовых» файлов от 50 до 100 раз и, похоже, упёрлись в потолок того, что можно исправить автоматически. Вместо дальнейшей настройки мы решили доделать оставшиеся файлы вручную, опираясь на базовые (пока падающие) варианты рефакторинга, чтобы быстрее довести их до финиша.

Результаты и влияние

Благодаря пайплайну валидации и рефакторинга, циклам повторных попыток и расширенному контексту нам удалось автоматически мигрировать 75% целевых файлов за 4 часа.

После четырёх дней доработки промптов и скриптов по схеме «выборка — настройка — массовый прогон» мы достигли отметки 97% из исходных 3,5 тыс. файлов Enzyme.

А для оставшихся 3% файлов, которые не удалось завершить автоматически, наши скрипты дали отличную базу для ручного вмешательства, что позволило завершить их миграцию ещё за неделю работы.

Самое важное — нам удалось заменить Enzyme, сохранив исходный замысел тестов и наше общее кодовое покрытие. И даже при большом числе повторных попыток на «длинном хвосте» миграции итоговая стоимость — включая использование API LLM и шесть недель инженерного времени — оказалась существенно эффективнее по сравнению с нашей первоначальной оценкой ручной миграции.

Что дальше

Эта миграция подчёркивает потенциал LLM для крупномасштабных преобразований кода. Мы планируем расширять этот подход, развивать более продвинутые инструменты миграции и исследовать новые сценарии автоматизации на базе LLM для повышения продуктивности разработчиков.


Меньше магии промптов, больше воспроизводимых процессов, метрик и экономики качества — на курсе «QA Lead». Курс помогает перевести разовые миграции и ретраи в управляемую практику команды: стратегия тестирования, данные для решений, автоматизация и её ROI, коммуникации со стейкхолдерами. Научитесь эффективно управлять командами тестирования.

А освоить автоматизацию тестирования на JavaScript можно на практическом курсе от экспертов индустрии. Чтобы узнать, подойдет ли вам программа курса, пройдите вступительный тест.

Комментарии (0)