Есть момент в разработке, который всегда немного успокаивает. Запускаешь тесты. Ждёшь. И видишь:
✓ 128 tests passed
Хорошо. Можно двигаться дальше.
Конечно, любой разработчик знает, что зелёные тесты не доказывают отсутствие ошибок. Нельзя протестировать вообще всё. Можно забыть сценарий. Можно неправильно настроить окружение. Можно иметь integration-проблему, которую unit-тест никогда не увидит.
Но всё равно зелёный test suite — довольно сильный психологический сигнал: по крайней мере то, что мы проверяем, работает так, как ожидается.
С AI у меня появился ещё один повод относиться к этому сигналу осторожнее. Потому что теперь один и тот же исполнитель может неправильно понять требование, написать неправильную реализацию, написать тест под свою неправильную реализацию и успешно пройти собственный тест.
Получается очень красивая цепочка:
wrong assumption
↓
wrong implementation
↓
matching test
↓
✓ green
И внешне всё выглядит идеально.
Самое неприятное — никакой технической ошибки может не быть
Представим достаточно обычную задачу. Есть заказы. После отмены заказа нужно вернуть товар в доступный остаток. Звучит просто.
AI исследует код и делает вывод: если заказ получил статус cancelled, зарезервированные товары необходимо освободить. Выглядит логично. Он реализует:
order cancelled
↓
release reservation
Затем пишет тест:
given reserved order
when order is cancelled
then reservation is released
Тест проходит. Статический анализ проходит. Код аккуратный. Архитектура проекта соблюдена. На review отдельных методов тоже ничего особенно подозрительного нет.
Только есть одно бизнес-правило, которое AI не увидел. Не все отменённые заказы должны возвращать товар в доступный остаток. Например, часть отмен происходит уже после передачи товара во внешний процесс. Статус один. Но смысл разных сценариев отличается.
Настоящее правило выглядит скорее так:
order cancelled
↓
почему отменён?
↓
на каком этапе?
↓
можно ли физически вернуть товар?
↓
только тогда release reservation
AI этого не знал. Но это никак не помешало ему написать зелёный тест.
Тест подтвердил не требование, а предположение
Мне кажется, это принципиально важная разница.
Мы часто смотрим на тест примерно так:
requirement
↓
test
↓
implementation
Есть ожидаемое поведение. Тест его фиксирует. Код должен пройти тест. Но когда всю цепочку строит один AI, легко получить другую последовательность:
AI понял requirement
↓
AI сформировал assumption
↓
AI написал implementation
↓
AI написал test для implementation
Теперь тест находится не снаружи решения. Он является продолжением того же рассуждения. Если исходное понимание было правильным — прекрасно. Если нет — зелёный цвет ничего не исправит.
Это напомнило мне одну старую проблему
Вообще сама ситуация не новая. Разработчики всегда могли писать тесты под собственную реализацию. Я решил, что метод должен работать так. Написал код. Потом написал тест, подтверждающий именно такое поведение. Тест проходит. Ничего магического в появлении AI здесь нет.
Но AI делает этот цикл намного быстрее. Раньше между «я думаю, что должно быть так» и «у меня есть реализация и десять зелёных тестов» могло пройти несколько часов. Теперь — несколько минут. А скорость создаёт очень неприятную иллюзию уверенности.
Десять зелёных тестов выглядят убедительнее одного предположения
Допустим, AI сообщает: реализация завершена. Добавлено 12 тестов. Все 247 тестов проходят. PHPStan ошибок не обнаружил. Очень приятный отчёт. Визуально он выглядит как набор доказательств.
implementation ✓
tests ✓
static analysis ✓
existing suite ✓
И возникает естественное желание закрыть задачу. Но большая часть этих проверок отвечает на технические вопросы. Компилируется ли код? Соответствуют ли типы? Не сломали ли мы уже покрытые сценарии? Работает ли новое поведение так, как описано в новом тесте?
Ни одна из этих галочек сама по себе не отвечает на главный вопрос: а новое поведение вообще правильное?
Особенно коварны тесты, которые выглядят очень хорошо
Плохой тест иногда заметить легко. Он слишком близок к реализации. Много моков. Проверяет внутренние вызовы вместо поведения. Ничего не говорит о реальном сценарии.
Но AI способен написать вполне хороший тест. Понятный given / when / then. Хорошее название. Минимум лишних деталей. Например:
it_releases_inventory_when_order_is_cancelled
Отличное название. Только само утверждение может быть неправильным. Может быть, должно быть:
it_releases_inventory_when_cancelled_before_fulfillment
Это уже совсем другое бизнес-правило. И никакая красота тестового кода не поможет выбрать между ними.
Я стал внимательнее смотреть на названия тестов
Потому что название очень хорошо выдаёт предположение, которое AI считает истиной. Например: it_allows_retry_for_failed_notifications. Стоп. Для всех failed notifications? Почему?
Или: it_marks_order_as_completed_after_payment. Точно после payment? А доставка? Или: it_allows_admin_to_cancel_any_order. Действительно любой?
Иногда я ещё не успел прочитать сам тест, а его название уже показывает место, которое стоит проверить. AI превращает собственное понимание задачи в исполняемое утверждение. И это очень полезно. Если помнить, что это всё ещё утверждение, а не истина.
Поэтому я начал задавать другой вопрос
Раньше после реализации естественный вопрос был: все тесты проходят? Теперь мне гораздо интереснее: откуда взялся expected result этого теста?
Вот, например:
self::assertSame(
OrderStatus::Cancelled,
$order->status(),
);
Почему мы ожидаем Cancelled? Потому что так написано в требовании? Потому что это существующее бизнес-правило? Потому что такой сценарий уже есть? Потому что соседние тесты подтверждают его? Или просто потому, что AI именно так реализовал feature? Это совсем разные основания.
Хороший тест должен иметь источник истины за пределами собственного кода
В идеальном мире у нас есть specification. Написано: если X и Y, система должна выполнить Z. Тогда можно сделать specification → test → implementation. Но реальные проекты редко настолько чистые.
Требование может быть в задаче. Часть — в документации. Часть — в существующем поведении. Часть — в разговоре с product owner. Часть — в голове разработчика. И coding agent приходится всё это восстанавливать.
Именно поэтому этап исследования до написания тестов становится таким важным. Сначала нужно понять: какое поведение мы считаем правильным? А уже потом автоматизировать его проверку. Это хорошо стыкуется с тем, почему я не хочу, чтобы AI проверял только самого себя.
Я стал разделять два вида тестов
Не технически. Скорее для себя. Есть тесты, которые проверяют: я правильно реализовал выбранное поведение? И есть тесты, которые помогают ответить: выбранное поведение вообще соответствует требованию?
Первый тип AI пишет очень хорошо. Например, мы уже договорились: retry разрешён только для состояния failed. Теперь можно попросить AI покрыть успешный retry, запрет для pending, повторный вызов, ошибку transport, права доступа. Здесь задача понятна.
Но если вопрос ещё находится на уровне «какие failed операции можно повторять?» — писать двадцать тестов рано. Сначала нужно установить правило.
Иногда первый полезный тест — это старый тест
Это вообще одна из вещей, которые я стал ценить намного сильнее. Когда AI реализует feature, ему очень хочется создать новые тесты. Но существующие тесты иногда важнее. Потому что они были написаны до текущего решения. То есть потенциально являются более независимым источником информации.
Например, агент считает: после оплаты заказ можно сразу завершить. Но в старом integration-тесте есть:
paid
↓
processing
↓
shipped
↓
completed
Это сильный сигнал. Можно обсуждать, актуален ли тест. Но он хотя бы не был создан специально под новую реализацию пять минут назад. Поэтому я всё чаще прошу AI перед изменением: найди связанные существующие тесты и сначала опиши, какое поведение они фиксируют. Это очень хорошо связывается с research phase.
Старый зелёный тест и новый зелёный тест — не всегда одинаково убедительны
Допустим, мы изменили feature. Все старые тесты проходят. И новые тоже. Это уже хороший сигнал. Почему? Потому что старые тесты создавались не как доказательство текущего решения. Они фиксируют предыдущий набор ожиданий.
Новый код должен одновременно соответствовать старым ожиданиям и новому требованию. Это уже намного интереснее, чем просто новый код плюс новый тест под него. И именно поэтому regression suite для AI-разработки становится особенно ценным.
Хотя старые тесты тоже могут быть неправы
Разумеется. Тест не становится истиной только потому, что ему четыре года. В legacy это особенно заметно. Иногда старый тест фиксирует баг, к которому все привыкли. Иногда бизнес изменился. Иногда описание теста давно не соответствует тому, что он реально проверяет.
Поэтому я не хочу превращать эту мысль в «старые тесты хорошие, новые AI-тесты плохие». Совсем нет. Речь скорее о независимости источников. Чем больше разные источники подтверждают одно поведение, тем выше моя уверенность.
Я стал просить одного агента критиковать тесты другого
После экспериментов с cross-review это оказалось довольно естественным продолжением. Например, Claude Code пишет implementation и tests, Codex делает review tests.
Причём запрос можно сделать специально узким: не проверяй пока production-код. Посмотри только на добавленные и изменённые тесты. Какое поведение они считают правильным? Соответствует ли оно исходной задаче? Какие предположения тесты делают неявно? Какие сценарии отсутствуют?
Мне нравится этот формат. Второй агент не начинает спорить о том, нужен ли здесь factory. Он сначала проверяет сам контракт поведения. Иногда именно на этом этапе задача разваливается. И это полезно. Потому что production-код ещё можно переделать.
Ещё интереснее попросить reviewer написать тест раньше автора
Для более рискованных задач можно перевернуть процесс. Не implementation → tests, а requirement → independent test cases → implementation.
Например, один агент получает только задачу и пишет список сценариев: что должно работать, что должно быть запрещено, пограничные случаи, повторные операции, ошибки, сохранение старого поведения. После этого другой агент пишет реализацию.
Теперь хотя бы часть ожидаемого поведения сформулирована независимо от кода. Это уже начинает напоминать обычный TDD, только с разделением ролей между агентами. Я пока не делаю так для каждой задачи. Но для сложной бизнес-логики идея мне нравится.
Test coverage тоже может красиво обманывать
Допустим, AI написал тесты. Coverage вырос с 78% до 83%. Выглядит отлично. Но coverage отвечает на вопрос: какие строки выполнились? Он не отвечает: какие важные бизнес-сценарии были забыты?
Можно добиться 100% покрытия вот такой логики:
if failed
allow retry
else
deny retry
И полностью пропустить тот факт, что существует несколько разных типов failed. Coverage прекрасный. Модель требований неправильная. Это очень хорошая иллюстрация разницы между технической полнотой и смысловой полнотой.
Статический анализ здесь тоже бессилен
PHPStan может сказать: типы корректны. Psalm может сказать: здесь нет очевидной проблемы с типами. IDE ничего не подсвечивает. Тесты зелёные. Но если система отправила клиенту уведомление, которое не должна была отправлять, никакая строгая типизация сама по себе это не поймает.
Это другой класс ошибки:
не «код сломан»
а
«код делает не ту вещь»
И чем лучше AI становится в техническом написании кода, тем заметнее для меня именно эта категория проблем.
Раньше моё внимание в основном было сосредоточено на реализации
Смотришь diff. Этот метод нормальный? Здесь нет N+1? Правильно ли обработано исключение? Не нарушена ли архитектура? Есть ли тест?
Теперь я всё чаще хочу подняться на один уровень выше. Например, вижу тест it_sends_reminder_after_24_hours. Первый вопрос: код корректно считает 24 часа? А всё чаще должен быть другой: почему вообще 24?
Это требование? Настройка? Существующее правило? Догадка модели? Потому что можно написать идеальный DateInterval. Но если бизнес просил 48 часов, вся техническая красота бессмысленна.
AI заставил меня снова задуматься о specification
Слово немного тяжёлое. Не каждой feature нужен формальный документ. Но идея очень простая. Перед тем как проверять реализацию, нужно иметь нечто, что существует независимо от неё и отвечает: что должно произойти?
Это может быть хорошая постановка задачи, acceptance criteria, существующий тест, пример входа и ожидаемого результата, бизнес-правило в документации, согласованный план, явный список сценариев. Не обязательно огромный документ. Иногда достаточно пяти строк. Но эти пять строк становятся чрезвычайно важными, когда и код, и проверки генерирует один и тот же агент.
Acceptance criteria неожиданно стали для меня намного ценнее
Например, вместо «добавить повторную отправку уведомления» лучше:
Повторная отправка доступна только для failed уведомлений.
Уведомление должно всё ещё быть актуально для текущего состояния сущности.
Успешно отправленное уведомление нельзя отправить повторно.
Каждая ручная попытка должна записываться в историю.
Существующий автоматический retry не должен измениться.
Теперь AI может написать тесты. И я могу сравнить их не только с кодом. Я могу буквально пройтись: criterion 1 → какой тест? criterion 2 → какой тест? ... criterion 5 → какой regression test?
Это намного сильнее успокаивает меня, чем просто «добавлено 14 tests».
Количество тестов вообще стало интересовать меня меньше
Иногда AI особенно гордо сообщает: добавлено 18 новых тестов. Раньше это звучало почти автоматически хорошо. Теперь первая реакция: на какие 18 утверждений о системе?
Потому что можно написать восемнадцать вариантов одного и того же неправильного предположения. Например, failed email → retry, failed sms → retry, failed push → retry, failed webhook → retry... И получить великолепное покрытие идеи «любую ошибку нужно повторять».
А настоящий пропущенный вопрос остаётся один: какие из этих операций вообще безопасно повторять? Один правильный тест иногда ценнее восемнадцати производных.
Зелёные тесты всё равно остаются необходимыми
Я не хочу, чтобы заголовок этой статьи звучал как «тесты больше ничего не значат». Значат. Очень много. Я всё так же не хочу принимать изменение с падающим test suite. Я всё так же считаю тесты одной из главных опор при работе с AI. Наверное, даже больше, чем раньше.
Просто значение зелёного цвета стало для меня немного другим. Раньше он эмоционально звучал как «всё хорошо». Теперь скорее: всё хорошо в рамках тех ожиданий, которые мы записали в тестах. И сразу появляется следующий вопрос: а ожидания правильные?
Получилась новая цепочка доверия
Для достаточно серьёзной задачи я бы сейчас хотел видеть что-то примерно такое:
requirement
↓
явные acceptance criteria
↓
research существующего поведения
↓
plan
↓
implementation
↓
tests
↓
independent review
↓
сверка tests с requirement
↓
verification
На первый взгляд это выглядит намного тяжелее, чем «мы же просто хотели добавить кнопку». Иногда действительно тяжелее. И для маленькой задачи всё это не нужно. Но если изменение затрагивает реальную бизнес-логику, цена уверенного неправильного решения может быть намного выше стоимости нескольких дополнительных проверок.
Самый опасный результат AI — не красная ошибка
Красную ошибку видно. Тест упал. Программа не запускается. Типы не сходятся. Это неприятно, но понятно. Гораздо интереснее другое:
✓ implementation complete
✓ tests passed
✓ static analysis passed
✓ review complete
И всё это вокруг неправильного понимания требования. Вот такой результат выглядит очень убедительно. И именно поэтому требует больше осторожности.
AI становится всё лучше в создании внутренне согласованных решений. Код соответствует тестам. Тесты соответствуют выбранной модели поведения. Документация может даже соответствовать коду. Но вся эта согласованная конструкция всё ещё может не соответствовать реальности.
Мне всё меньше важно, может ли AI доказать, что его код работает
Гораздо интереснее: может ли он доказать, что проверяет правильную вещь?
И здесь одного агента мне уже недостаточно. Нужен внешний источник. Требование. Документация. Существующее поведение. Другой агент. Человек. Что-то, что не возникло из той же самой цепочки предположений.
Потому что если автор ошибки сам написал доказательство своей правоты, количество зелёных галочек не обязательно делает это доказательство сильнее.
Поэтому зелёные тесты всё ещё радуют меня
Просто теперь немного меньше успокаивают. Когда вижу ✓ 128 tests passed, я по-прежнему предпочитаю это красному CI. Но следом в голове появляется ещё один вопрос, которого раньше я задавал себе реже: а что именно эти 128 тестов сейчас доказали?
И особенно: кто решил, что ожидаемый результат в новых тестах правильный? Если ответ — тот же AI, который придумал решение, написал код и потом проверил самого себя, то для меня это уже не конец проверки. Это только один её слой.
Потому что AI вполне способен очень аккуратно пройти путь wrong assumption → wrong implementation → matching test → green. И чем лучше он становится в написании кода и тестов, тем важнее не перепутать внутреннюю согласованность с правильностью.