Модуль 1 · Основы · урок 4 из 13
Юнит- и интеграционные тесты
Юнит-тесты — то, что агент пишет охотнее всего: функция перед глазами, вход и выход понятны, прогон занимает секунды. Именно поэтому здесь легче всего получить сотню зелёных тестов, которые ничего не ловят. В уроке — как ставить задачу, что просить явно и как отличить полезный тест от декоративного.
Что говорит практика OpenAI
В гайде о том, как инженеры OpenAI используют Codex, повышение покрытия тестами — отдельный сценарий. Инженеры просят тесты на граничные случаи и вероятные пути отказа, генерируют юнит- и интеграционные тесты по сигнатурам функций и окружающей логике, просят property-based тест для утилиты сортировки или расширяют тестовый файл проверками на null и недопустимые состояния (How OpenAI uses Codex). Гайд по лучшим практикам добавляет: агент должен знать, как выглядит «хорошо», а задача — заканчиваться прогоном нужных проверок (Best practices).
Как поставить задачу
Хорошая постановка состоит из цели, контекста, ограничений и условия «готово, когда»:
Цель: покрыть юнит-тестами сервис нормализации телефонов
src/plugins/<vendor>/<plugin>/classes/PhoneNormalizer.php.
Контекст: посмотри существующие тесты в tests/Unit — повтори их стиль,
data provider'ы и именование. Требование: на выходе формат +7XXXXXXXXXX
или null, если номер некорректен.
Ограничения: код сервиса не меняй. Если найдёшь баг — опиши его отдельно,
тест на него пометь и не подгоняй ожидание под текущее поведение.
Готово, когда: тесты прогнаны командой vendor/bin/phpunit tests/Unit,
вывод приложен; перечислены случаи, которые ты сознательно не покрыл.Фраза «не подгоняй ожидание под текущее поведение» — главная. Без неё агент, встретив расхождение, исправит ожидание в тесте, и баг станет задокументированной нормой.
Результат обычно выглядит так:
<?php
declare(strict_types=1);
namespace Tests\Unit;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Vendor\Plugin\Classes\PhoneNormalizer;
final class PhoneNormalizerTest extends TestCase
{
#[DataProvider('cases')]
public function testNormalize(string $input, ?string $expected): void
{
self::assertSame($expected, (new PhoneNormalizer())->normalize($input));
}
public static function cases(): array
{
return [
'восьмёрка и скобки' => ['8 (999) 123-45-67', '+79991234567'],
'уже в формате' => ['+79991234567', '+79991234567'],
'без кода страны' => ['9991234567', '+79991234567'],
'пустая строка' => ['', null],
'слишком короткий' => ['12345', null],
'буквы внутри' => ['8999abc4567', null],
];
}
}Смотрите не на количество строк, а на то, откуда взялись ожидания. Здесь каждое ожидание следует из требования «формат +7 или null», а не из кода сервиса.
Граничные случаи — просите списком
Агент действительно хорошо находит границы, но только если о них спросить. Добавьте в промпт перечень измерений: пустое значение, null, максимальная длина и длина + 1, юникод и эмодзи, пробелы по краям, отрицательные числа и ноль, часовые пояса и переход через полночь, повторный вызов. Для TypeScript-утилит попросите property-based тест — в JS-мире для этого есть, например, библиотека fast-check. Свойство вроде «нормализация идемпотентна: повторный вызов не меняет результат» ловит то, о чём не подумал никто.
Интеграционные тесты: база и внешние сервисы
С интеграционными тестами агенту нужно больше контекста, чем с юнитами. Запишите в AGENTS.md:
- как поднимается тестовая база и сбрасывается ли она между тестами (в Laravel — транзакции или
RefreshDatabase); - какие внешние сервисы есть у модуля — CRM, почта, платёжный шлюз — и чем они подменяются в тестах (в Laravel HTTP-клиент подменяется через
Http::fake()); - что нельзя делать никогда: слать реальные письма, ходить в боевую CRM.
Типичная задача — интеграция с CRM. Попросите агента сначала перечислить, какие ответы может вернуть внешний сервис: успех, 4xx с ошибкой валидации, 5xx, таймаут, неожиданный формат. Затем — тест на каждый ответ с проверкой поведения вашей стороны: повтор, запись в лог, понятная ошибка пользователю, отсутствие дубля заявки.
Признаки декоративного теста
Пройдитесь по тому, что написал агент, с этим списком:
- Ожидание вычислено той же формулой, что и в коде. Тест повторяет реализацию и всегда с ней согласен.
- Проверяется мок, а не код. Замокано всё, включая тестируемую логику; тест проверяет, что мок вернул то, что ему велели.
- Утверждение без содержания:
assertNotNull($result),assertTrue(true),expect(result).toBeDefined()там, где важно значение. - Изменён код продукта, чтобы тест прошёл, хотя вы об этом не просили. Проверяйте
git diff --stat— правки внеtests/видны сразу. - Пропущенные тесты (
markTestSkipped,test.skip) без объяснения.
Самая надёжная проверка — убедиться, что тест падает, если сломать код. Подробно, с мутационным тестированием, — в уроке «Как проверять тесты, написанные агентом». Если вы пишете тест под новую фичу, порядок «сначала красный тест, потом код» разобран в уроке «Тесты как спецификация: TDD с агентом».
Большие объёмы: субагенты и worktree
Покрыть тестами весь старый модуль за один заход — плохая идея: контекст забивается выводом тестов, и качество падает к концу. Гайд по лучшим практикам советует отдавать тесты и разбор отказов субагентам, а параллельные задачи разносить по git worktree, чтобы они не мешали друг другу. На практике: один модуль — одна задача — один MR, который можно спокойно проверить.
Попробуйте сами
10–15 мин на рабочем месте- Выберите в своём проекте маленький модуль без тестов. Поставьте задачу по шаблону «цель, контекст, ограничения, готово, когда» и проверьте
git diff --stat: менял ли агент что-то вне тестов. - Попросите агента перечислить граничные случаи для одной функции до написания тестов и сравните со списком, который составили бы сами.
- Найдите в написанных тестах хотя бы одно утверждение, которое не упадёт при неправильном результате.
Коротко
- Задача на тесты: цель, контекст (стиль существующих тестов), ограничения, условие «готово».
- Запрещайте подгонять ожидания под текущее поведение — найденный баг описывается отдельно.
- Граничные случаи и property-based тесты просите явно, со списком измерений.
- Для интеграционных тестов агенту нужны правила базы и подмены внешних сервисов в AGENTS.md.
- Ожидание из кода, проверка мока и пустые утверждения — признаки теста, который ничего не ловит.