29 нояб. 2024 г.·7 мин чтения

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

Практичный набор фронтенд-тестов для команд, которые используют код, сгенерированный ИИ: понятные роли для unit-, component- и browser-тестов.

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

Почему сгенерированные диффы проскальзывают через ревью

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

Этот разрыв становится больше, когда речь идет о тестировании кода, сгенерированного ИИ. Сгенерированные диффы часто выглядят аккуратно и уверенно, даже если в них спрятано слабое допущение. Если код компилируется, а текст выглядит правдоподобно, ревьюеры обычно двигаются быстрее, чем следовало бы.

Чаще всего проблема — в мелких визуальных багax. Обертка смещается на несколько пикселей, сообщение об ошибке сдвигает кнопку вниз, или после рефакторинга исчезает ring фокуса. В диффе это не бросается в глаза, если ревьюер не знает страницу очень хорошо.

Состояния тоже легко пропустить. Форма все еще отправляется, но индикатор загрузки может появляться слишком поздно. Модальное окно все еще открывается, но фокус клавиатуры может попасть на страницу позади него. Это настоящие регрессии, и пользователи замечают их сразу.

ИИ еще и создает ложное ощущение покрытия, потому что часто обновляет рядом лежащие тесты вместе с кодом функции. Эти тесты могут по-прежнему проходить, но не проверять путь, который ломается в реальном использовании. Модель обычно идет по самому очевидному сценарию, а не по редкому случаю, который пользователь ловит со второй попытки.

Ревьюеры также читают код по отдельности. Обычно они не прогоняют каждый связанный экран, не меняют размер окна, не проходят поля Tab-ом, не вызывают серверные ошибки и не повторяют одно и то же действие дважды. Они ищут ошибки в логике, именовании и явные поломки. Это полезно, но многие UI-регрессии так не ловятся.

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

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

Что должен ловить каждый тип тестов

Код от ИИ часто выглядит нормально в диффе, но все равно ломает страницу мелкими, раздражающими способами. Хороший набор фронтенд-тестов работает потому, что у каждого типа тестов своя узкая задача. Когда тесты слишком сильно дублируют друг друга, каждая ошибка говорит одно и то же, и никто не понимает, куда смотреть первым делом.

Unit-тесты должны ловить маленькие ошибки в логике. Речь о правилах ввода, форматировании дат, расчетах цен, сопоставлении полей и вспомогательных функциях, которые превращают сырые данные в текст для отображения. Если AI-инструмент меняет правило валидации с ">= 8" на "> 8", unit-тест должен упасть еще до того, как кто-то откроет браузер.

Component-тесты стоят на уровень выше. Они проверяют, что видит пользователь в одной части интерфейса, когда меняются props, state или заглушенные ответы. Сюда входят состояния загрузки, пустые состояния, отключенные кнопки, сообщения об ошибке и текст, который появляется после действия пользователя. Если сгенерированный код забывает показать "Email is required" или оставляет кнопку отправки активной во время ожидающего запроса, component-тест должен это поймать.

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

Простое правило делает ошибки понятнее:

  • Unit-тесты проверяют чистую логику и вспомогательные функции.
  • Component-тесты проверяют одну часть экрана и ее видимое поведение.
  • Browser-тесты проверяют полный пользовательский сценарий.

Такое разделение делает вывод тестов понятным. Если падает unit-тест, разработчик смотрит на логику. Если падает component-тест, он проверяет рендеринг и состояние. Если падает browser-тест, он смотрит на интеграцию и реальный сценарий.

Небольшое пересечение допустимо. Полное разделение нереалистично. Например, форма регистрации может проверять формат email в unit-тесте, показывать правильное inline-сообщение в component-тесте и доказывать, что вся форма отправляется и перенаправляет пользователя, в browser-тесте. Этого достаточно. Не нужны три разных теста, которые все доказывают одну и ту же ошибку на одном и том же экране.

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

Как разделить тесты по уровням

Хороший набор фронтенд-тестов переносит основную нагрузку в самый быстрый слой. Для многих команд это значит много unit-тестов, меньше component-тестов и совсем небольшой набор browser-тестов. Такой баланс особенно важен при генерации кода ИИ, потому что ревьюерам нужны быстрые сигналы, пока они читают дифф.

Unit-тесты должны нести наибольшую нагрузку. Сюда стоит помещать бизнес-правила, валидацию ввода, форматирование, обновление состояния и маленькие вспомогательные функции. Если сгенерированный код меняет условие, путает названия полей или ломает mapper, unit-тесты обычно ловят это за секунды.

Component-тесты находятся посередине не просто так. Используйте их для того, что люди действительно могут увидеть и нажать: состояния загрузки, сообщения об ошибке, отключенные кнопки, пустые результаты, длинный текст, странные ответы API и формы с пропущенными значениями. Они дороже unit-тестов, но ловят многие UI-баги до того, как вы потратите время в настоящем браузере.

Browser-тесты должны оставаться короткими и отобранными. Оставьте их для сценариев, где баг бьет по пользователям или по деньгам.

  • Помещайте общую логику и изменения состояния в unit-тесты.
  • Помещайте видимые состояния и неудобные крайние случаи в component-тесты.
  • Помещайте регистрацию, оплату, биллинг и сброс пароля в browser-тесты.
  • Добавляйте все три слоя, когда один сбой может затронуть много пользователей.

Последний пункт особенно важен. Не распределяйте тесты равномерно только потому, что так выглядит аккуратно. Усиливайте покрытие там, где радиус поражения выше. Сломанная подсказка неприятна, но неправильный расчет цены или мертвый шаг регистрации могут в тот же день остановить выручку.

Простой пример хорошо показывает разницу. Если форма регистрации считает надежность пароля, unit-тесты должны проверять правила. Component-тесты должны проверять слабое, валидное, загрузочное и серверное состояние. Один browser-тест должен доказать, что реальный пользователь может открыть страницу, заполнить форму, отправить ее и увидеть экран успеха.

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

Настройте процесс ревью шаг за шагом

Ревью работает лучше, когда команда проверяет одни и те же вещи в одном и том же порядке. ИИ может быстро написать много кода, но из-за скорости мелкие поломки интерфейса легче пропустить.

Начинайте с того, что человек действительно увидит. Прежде чем кто-то прочитает реализацию, ревьюер должен ответить на один простой вопрос: что изменилось на экране и что теперь пользователь может делать иначе? Это помогает сосредоточиться на поведении, а не на стиле.

Простой процесс выглядит так:

  1. Отметьте в диффе изменение, заметное пользователю. Если патч меняет текст, верстку, правила формы, состояния загрузки или поведение кнопки, первым делом укажите это в pull request.
  2. Запускайте unit-тесты на каждое сгенерированное изменение. Они ловят сломанные помощники, неправильные условия и крайние случаи, которые могут не проявиться при быстром визуальном просмотре.
  3. Запускайте component-тесты для измененного экрана и для всех общих частей интерфейса, которые он использует. Если сгенерированный дифф затрагивает модальное окно, таблицу, поле формы или кнопку, используемую в нескольких местах, этот шаг важнее, чем размер самого диффа.
  4. Запускайте browser-тесты только тогда, когда изменение влияет на весь сценарий. Вход в аккаунт, оформление заказа, онбординг, поиск и настройки аккаунта — хорошие примеры. Если вместе работают несколько экранов, browser-тест себя оправдывает.
  5. Останавливайте merge, если падает пользовательский тест. Не считайте упавший UI-тест необязательным только потому, что изменение кода выглядит маленьким.

Такой порядок экономит время. Unit-тесты работают быстро, поэтому сначала убирают очевидные ошибки. Component-тесты проверяют отрендеренный результат без шума полноценного запуска браузера. Browser-тесты остаются сосредоточенными на реальных маршрутах, и благодаря этому набор тестов становится меньше и вызывает больше доверия.

Команды часто делают наоборот. Они либо запускают только browser-тесты и слишком долго ждут обратной связи, либо останавливаются на unit-тестах и пропускают сломанный текст, отступы или пути кликов. Смешанный набор фронтенд-тестов дает ревьюерам достаточно доказательств, чтобы одобрять сгенерированные диффы без догадок.

Если вы хотите закрепить этот процесс, добавьте его в шаблон pull request и в правила CI. Тогда каждое сгенерированное изменение будет проходить через один и тот же фильтр, даже в загруженные дни.

Простой пример на форме регистрации

Проверить изменения, сгенерированные ИИ
Oleg может заметить рискованные изменения интерфейса, слабые тесты и пробелы в вашем процессе проверки.

Форма регистрации — хорошее место, чтобы увидеть набор фронтенд-тестов в действии, потому что даже маленькие изменения могут быстро сломать реальное поведение. Представьте, что AI-помощник рефакторит форму так, что кнопка отправки остается отключенной, пока поля не выглядят валидными, и заодно переписывает правило проверки email.

Дифф может выглядеть аккуратно. Ревьюер видит более чистый код, меньше строк и более удобную работу со state. Именно поэтому такие изменения и проскакивают через ревью.

Начните с unit-теста для правила email. ИИ изменил проверку и теперь отклоняет адреса вроде "[email protected]" или принимает "sam@example", хотя не должен. Unit-тест изолирует эту логику и падает за секунды. Ревьюеру не нужно разбирать каждую ветку в функции валидации. Ошибка указывает на одно правило и один файл.

Затем добавьте component-тест для самой формы. В этой версии форма выставляет правильное состояние ошибки, но сообщение об ошибке остается скрытым, потому что рефакторинг оставил не то условие на блоке сообщения. Тест заполняет форму плохим email, переводит фокус с поля и ожидает, что текст ошибки появится на экране. Если текст есть в коде, но пользователь его не видит, component-тест это поймает.

Browser-тест покрывает то, что ревьюеры обычно пропускают. На ширине мобильного экрана ИИ поменял верстку, и порядок перехода по Tab сломался. Человек, который пользуется клавиатурой, переходит от поля email к полю пароля, а потом фокус прыгает в футер вместо кнопки отправки. Эта проблема не покажется в unit-тесте, а component-тест может ее пропустить, если он не запускается в настоящем браузере.

Одна небольшая форма теперь дает ревьюеру три ясных сигнала:

  • unit-тест: неправильно работает правило email
  • component-тест: скрыто сообщение об ошибке
  • browser-тест: ломается фокус клавиатуры на мобильном экране

В этом и смысл набора фронтенд-тестов. Каждый сбой относится к разному уровню изменения, поэтому проверять сгенерированные диффы становится проще. Ревьюер перестает гадать и проверяет точное поведение, которое изменилось.

Ошибки, которые тратят время

Плохой набор фронтенд-тестов обычно ломается не из-за нехватки тестов. Он ломается потому, что команды ставят не тот тест на не тот уровень. С ИИ-сгенерированным кодом это становится хуже: диффы выглядят правдоподобно, и ревьюеры двигаются быстрее.

Одна из частых ошибок — писать browser-тесты для каждой мелкой ветки. Browser-тест должен доказывать, что пользователь может закончить реальную задачу, а не то, что отрисовываются все варианты кнопки. Если сгенерированный дифф меняет подсказку, пустое состояние или индикатор загрузки, component-теста обычно достаточно. Оставьте browser-покрытие для сценариев вроде регистрации, оплаты или сброса пароля.

Команды также теряют время, когда пропускают component-тесты, потому что экран кажется простым. Простые экраны ломаются постоянно. Сгенерированное изменение может заменить подпись, убрать сообщение об ошибке или повесить не тот callback на кнопку отправки. Страница по-прежнему выглядит нормально при ревью, но поведение уже сломано. Точный component-тест ловит это быстрее, чем полный browser-запуск.

Тесты, написанные ИИ, тоже нужно проверять. Модели часто создают тесты, которые просто проверяют, что что-то отрендерилось, хотя риск бага находится в изменении состояния или в тексте ошибки. Читайте assertions построчно. Если тест пройдет даже после сломанного рефакторинга, удалите его и перепишите.

Большие snapshot-тесты — еще одна потеря времени. Когда snapshot покрывает половину страницы, никто не читает его внимательно. Ревьюеры просто жмут Accept. Делайте snapshot маленьким или вообще отказывайтесь от него и проверяйте только то, что действительно важно: видимый текст, состояние disabled и действия пользователя.

Нестабильные тесты создают самую плохую привычку: перезапускать до зеленого. Это скрывает реальные дефекты и приучает команду игнорировать падения. Если тест ведет себя нестабильно, исправьте причину в тот же день. Чаще всего дело в ненадежных селекторах, проблемах с таймингом, общем состоянии или тестовых данных, которые протекают между запусками.

Работает более короткий набор правил:

  • Помещайте основную UI-логику в unit- и component-тесты.
  • Оставляйте browser-тесты для сценариев, на которых можно заработать или потерять деньги, пользователей или доверие.
  • Отклоняйте расплывчатые assertions, сгенерированные ИИ.
  • Считайте нестабильные тесты сломанным кодом, а не невезением.

Такая дисциплина экономит часы каждую неделю, и команда снова начинает доверять тестовому набору.

Быстрая проверка перед merge

Получите помощь Fractional CTO
Привлеките опытного технического лидера для разработки с ИИ, тестирования и решений по релизам.

Сгенерированные диффы часто выглядят нормально в pull request и при этом все равно ломают экран мелкими, раздражающими способами. Кнопка сдвигается на несколько пикселей, фокус попадает не в то поле, или сообщение об ошибке вообще не появляется. Хороший набор фронтенд-тестов ловит эти проблемы до того, как они дойдут до пользователей.

Проверяйте дифф, задавая один простой вопрос: что изменилось для человека, который пользуется страницей? Если в ответе есть текст, верстка, фокус или валидация, тесты должны покрывать именно это поведение. Если дифф меняет только внутренний код, возможно, новый browser-тест вообще не нужен.

Чек-лист перед merge

Перед merge сделайте пять быстрых проверок:

  • Сопоставьте тесты с изменением, видимым пользователю. Если дифф меняет текст, отступы, порядок Tab-ов или правила формы, добавьте или обновите тест, который покрывает это поведение.
  • Покройте весь набор состояний. Состояния загрузки, пустого экрана, ошибки и успеха ломаются чаще, чем happy path.
  • Прогоните один browser-тест по полному сценарию. Для формы регистрации это значит: ввести данные, отправить форму, увидеть валидацию, исправить ошибки и дойти до успеха.
  • Спросите, зачем существует каждый новый тест. Ревьюер должен объяснить это одним предложением, без догадок.
  • Убедитесь, что тест падает по правильной причине. Если большинство падений вызывают таймаут, mock или хрупкий селектор, такой тест — это шум.

Небольшой пример хорошо это показывает. Допустим, AI-инструмент обновляет форму регистрации и меняет правило пароля с 8 символов на 12. Один unit-тест может проверить само правило. Component-тест может проверить inline-текст ошибки и фокус. Один browser-тест может подтвердить, что вся форма работает от начала до конца.

Если вы добавляете три теста, и все они ломаются, когда меняется подпись на кнопке, значит, набор настроен неправильно. Каждый тест должен защищать один уровень риска, а не повторять одну и ту же проверку.

Ревьюеры должны читать и названия тестов. Плохие названия скрывают слабое покрытие. Хорошие названия сразу показывают, что сломалось: "shows password error after blur" говорит гораздо больше, чем "form works".

Эта последняя минутная проверка экономит время потом. Исправить хрупкий тест до merge проще, чем разбираться с шумной ошибкой после релиза.

На что смотреть после релиза

Постройте легкий поток тестов
Создайте процесс проверки, который подходит для фронтенд-работы с ИИ и не замедляет релизы.

Релиз показывает, совпадают ли ваши тесты с проблемами, с которыми на самом деле сталкиваются пользователи. Если набор тестов зеленый, а люди все равно застревают, разрыв обычно легко увидеть, если смотреть на реальные действия, а не на количество тестов.

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

  • неудачные действия пользователей, например отправки, которые так и не завершаются
  • ошибки на стороне клиента в браузере
  • резкое падение количества завершенных сценариев
  • обращения в поддержку или баг-репорты с пометкой UI issues

Одна ошибка в браузере может значить больше, чем двадцать прошедших тестов. Если пользователи нажимают "Save" и ничего не происходит, это реальная поломка, даже если unit-тесты говорят, что handler существует.

Баг-репорты также показывают, где изменения от ИИ требуют большего внимания. Сгенерированные диффы часто затрагивают одни и те же типы кода: wiring форм, состояния загрузки, отключенные кнопки, условный рендеринг и сопоставление полей между UI и API. Когда баг попадает в одну из этих областей, сравните его с диффом, который его внес. Если один и тот же шаблон повторяется, ваш процесс проверки пропустил целый класс регрессий.

Вот где набор фронтенд-тестов становится практичным. Не оставляйте каждый тест только потому, что он уже есть в наборе. Если тест ни разу не ловит реальную проблему, удалите его. Snapshot-тесты, которые только подтверждают, что разметка изменилась, — частый пример. Они добавляют шум, замедляют ревью и приучают людей игнорировать падения.

Переносите усилия в тесты, которые поймали бы поздний баг. Если ревьюеры пропустили сломанный индикатор загрузки, который блокировал оплату, добавьте component- или browser-тест на это состояние. Если ИИ изменил helper и появилось неверное сообщение об ошибке, unit-теста может быть достаточно. Подбирайте новый тест под тот уровень, на котором баг действительно проскочил.

Превращайте данные релиза в изменения тестов

Держите цикл обратной связи простым. После каждого релиза задавайте два вопроса: что сломалось и какой тест мог бы поймать это раньше?

Если ответ — "ни один из наших тестов", добавьте покрытие именно в этом месте. Если ответ — "тест был, но он никогда не падает на реальных багax", перепишите его или удалите.

Через несколько релизов начинают проявляться закономерности. Сначала тратьте время на эти сценарии. Так команды делают тестирование кода, сгенерированного ИИ, менее шумным и гораздо более полезным.

Следующие шаги для вашей команды

Начните с малого. Возьмите один экран, который может нанести вред, если сломается, например регистрацию, оплату или сброс пароля. Затем разложите тесты по слоям. Unit-тесты должны покрывать мелкие правила, component-тесты — состояния и ввод пользователя, а browser-тесты — полный путь, который проходит реальный человек.

Такое упражнение обычно быстро показывает пробелы. Многие команды обнаруживают, что у них много unit-тестов, несколько browser-тестов и почти ничего посередине. Именно туда чаще всего проскальзывает сгенерированный UI-код, потому что в диффе он выглядит нормально, а экран ведет себя немного иначе.

Простой рабочий план выглядит так:

  • Выберите один рискованный экран и выпишите его happy path, состояние ошибки и один неудобный крайний случай.
  • Решите, какие browser-тесты должны запускаться всегда. Хорошие кандидаты — логин, регистрация, оплата, биллинг и любые многошаговые формы.
  • Держите component-тесты рядом с частями интерфейса, которые меняются чаще всего: валидация форм, отключенные кнопки, состояния загрузки и modal-flow.
  • Проверяйте тесты, написанные ИИ, с такой же внимательностью, как и UI-код. Смотрите, что они не покрывают, что они подменяют и упадут ли они на том баге, которого вы боитесь.

Запишите правило для browser-тестов простым языком, чтобы никто не гадал. Например: если изменение затрагивает auth, payments, routing, общую логику форм или критически важный экран, перед merge запускается browser-набор. Делайте правило коротким. Длинные регламенты никто не читает.

Относитесь к тестам, написанным ИИ, с легкой настороженностью. Они часто проходят по неправильной причине. Сгенерированный тест может пройтись по сценарию, но не проверить текст, который видит пользователь, или может замокать именно тот баг, который вам нужно было поймать. Если ревьюер усомнился бы в изменении интерфейса, он должен усомниться и в тесте.

Если вашей команде нужен взгляд со стороны, Oleg Sotnikov может проверить ваш набор фронтенд-тестов и поток CI в роли Fractional CTO. Такой внешний аудит особенно полезен, когда генерация кода ИИ ускоряет выпуск быстрее, чем успевает за ней процесс проверки.

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