Привет, хабровчане! Меня зовут Александр и я Flutter-разработчик. В этой статье хочу рассказать о том как я подружил ИИ-агентов с интеграционными тестами Flutter, какой инструмент пришлось для этого написать и что вообще из этого вышло. Летс гоу.

Проблема

Представьте, что вы попросили агента написать для вас интеграционный тест. На моих проектах очень часто это выглядело следующим образом:

  1. Агент изучает код

  2. Пишет тест

  3. Запускает flutter test

  4. Тест не проходит

  5. Агент пытается понять в чем дело, делает фикс

  6. Переходит к пункту 3

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

Таким образом определились следующие узкие места при разработке интеграционных тестов:

  • Сжигание токенов и контекстного окна на чтение всех логов

  • Время на пересборку

  • Непонимание только по логам на каком этапе теста возникла проблема

  • Время и токены на исправление несуществующих проблем и создание новых

  • Отсутствие у агента визуального представления того что на экране

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

Решение

Testwire - это утилита для пошагового исполнения интеграционных тестов. Тест разбивается на логические шаги, которые имеют состояния: не выполнен, выполнен, выполняется, выполнен с ошибкой. Агент запускает тест, подключается по MCP к тесту через VM Service и контролирует его выполнение.

Как это выглядит с точки зрения кода (о том почему тест пишется в виде отдельного класса чуть позже):

class MyTest extends TestwireTest {
  MyTest() : super(
    'Submit feedback form',
    setUp: (tester) async {
      app.main();
      await tester.pumpAndSettle();
    },
  );

  @override
  Future<void> body(WidgetTester tester) async {
    await step(
      description: 'Navigate to Leave Review',
      context: 'Tap the "Leave Review" tile on the home screen.',
      action: () async {
        await tester.tap(find.byKey(const Key('leave_review_tile')));
        await tester.pumpAndSettle();
      },
    );

    await step(
      description: 'Enter name',
      context: 'Type "Alex" into the name field.',
      action: () async {
        await tester.enterText(
          find.byKey(const Key('name_field')), 'Alex');
        await tester.pumpAndSettle();
      },
    );

    await step(
      description: 'Tap 5-star rating',
      context: 'Tap the 5th star to set rating to 5.',
      action: () async {
        await tester.tap(find.byKey(const Key('star_5')));
        await tester.pumpAndSettle();
      },
    );

    await step(
      description: 'Verify result',
      context: 'Check that the success message is displayed.',
      action: () async {
        expect(find.text('Thank you!'), findsOneWidget);
        expect(find.text('5 stars from Alex'), findsOneWidget);
      },
    );
  }
}

Доступные MCP инструменты:

Инструмент

Что делает

connect

Подключиться к тесту через VM service URI

step_forward

Следующий шаг, потом пауза

run_remaining

Выполнить все оставшиеся шаги (стоп при ошибке)

retry_step

Перепрогнать упавший шаг

get_test_state

Статус всех шагов

hot_reload_testwire_test

Hot reload с сохранением прогресса

hot_restart_testwire_test

Полный рестарт

screenshot

Скриншот UI

disconnect

Отключиться

Hot Reload - ключевая механика

Для разработки, в агентном режиме, тест запускается через flutter run, таким образом позволяя агенту подключиться к VM (в том числе через Dart MCP), считывать состояние, делать скриншоты.
Если какой-то шаг зафейлился, то выполнение теста приостанавливается и агент выясняет причины фейла уже имея доступ не только к логам но и к VM, а так же к визуальному состоянию. После фикса агент делает hot reload и делает ретрай последнего шага.

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

Как это работает

Архитектура Testwire
Архитектура Testwire

Три компонента:

  1. Тест - запускается через flutter run (не test!) с --dart-define=AGENT_MODE=true. Приложение стартует в дебаг-режиме, тест регистрирует экстеншены Dart VM service и ждет

  2. testwire_mcp - MCP сервер, который подключается к тесту через VM service, предоставляя агенту необходимые инструменты

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

Как это меняет мою разработку

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

Один файл - два режима

При этом на CI тест запускается в том же файле, но как обычный, без флага AGENT_MODE. То есть два режима:

# Агентский режим с hot reload
flutter run --dart-define=AGENT_MODE=true integration_test/my_test.dart

# Обычный CI прогон
flutter test integration_test/my_test.dart

Заключение

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

Буду рад фидбэку. Посмотреть примеры и как стартануть можно по ссылке в GitHub репозитории.

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


  1. Harrunos
    21.03.2026 23:16

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


    1. afppvv Автор
      21.03.2026 23:16

      Спасибо за обратную связь! Если я вас правильно понял и речь об аномалиях в самих тестах, то это хороший пойнт: на большем объёме такое легко можно пропустить. На данном этапе решаем это с помощью код-ревью - сначала сабагентом, потом человеком


      1. Harrunos
        21.03.2026 23:16

        Точно, код-ревью sub-agent + человек - надёжный подход на текущем этапе.
        Аномалии в поведении тестов часто вылезают именно при переходе на реальные сценарии. Например, агент кликает идеально по центру элемента 10 раз подряд - логически тест зелёный, но для реального UI (особенно web-based) это флаг для антибота. В browser automation такое ловим отдельной проверкой на human-like паттерны.

        Для этого как раз и используем CloakBrowser - он не только патчит fingerprint, но и добавляет behavioral noise (рандом в движении мыши, задержки) на уровне движка. Тогда агентский тест не только проходит, но и не палится как автоматика.


        1. afppvv Автор
          21.03.2026 23:16

          Да, понял, о чём вы. Спасибо, что поделились примером. Для browser automation это действительно звучит как отдельный слой валидации поведения