Amiga
Оглавление курса · урок 4 из 13

Модуль 1 · Основы · урок 4 из 13

Юнит- и интеграционные тесты

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

Что говорит практика OpenAI

В гайде о том, как инженеры OpenAI используют Codex, повышение покрытия тестами — отдельный сценарий. Инженеры просят тесты на граничные случаи и вероятные пути отказа, генерируют юнит- и интеграционные тесты по сигнатурам функций и окружающей логике, просят property-based тест для утилиты сортировки или расширяют тестовый файл проверками на null и недопустимые состояния (How OpenAI uses Codex). Гайд по лучшим практикам добавляет: агент должен знать, как выглядит «хорошо», а задача — заканчиваться прогоном нужных проверок (Best practices).

Как поставить задачу

Хорошая постановка состоит из цели, контекста, ограничений и условия «готово, когда»:

text
Цель: покрыть юнит-тестами сервис нормализации телефонов
src/plugins/<vendor>/<plugin>/classes/PhoneNormalizer.php.

Контекст: посмотри существующие тесты в tests/Unit — повтори их стиль,
data provider'ы и именование. Требование: на выходе формат +7XXXXXXXXXX
или null, если номер некорректен.

Ограничения: код сервиса не меняй. Если найдёшь баг — опиши его отдельно,
тест на него пометь и не подгоняй ожидание под текущее поведение.

Готово, когда: тесты прогнаны командой vendor/bin/phpunit tests/Unit,
вывод приложен; перечислены случаи, которые ты сознательно не покрыл.

Фраза «не подгоняй ожидание под текущее поведение» — главная. Без неё агент, встретив расхождение, исправит ожидание в тесте, и баг станет задокументированной нормой.

Результат обычно выглядит так:

php · tests/Unit/PhoneNormalizerTest.php
<?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 мин на рабочем месте
  1. Выберите в своём проекте маленький модуль без тестов. Поставьте задачу по шаблону «цель, контекст, ограничения, готово, когда» и проверьте git diff --stat: менял ли агент что-то вне тестов.
  2. Попросите агента перечислить граничные случаи для одной функции до написания тестов и сравните со списком, который составили бы сами.
  3. Найдите в написанных тестах хотя бы одно утверждение, которое не упадёт при неправильном результате.

Коротко

  • Задача на тесты: цель, контекст (стиль существующих тестов), ограничения, условие «готово».
  • Запрещайте подгонять ожидания под текущее поведение — найденный баг описывается отдельно.
  • Граничные случаи и property-based тесты просите явно, со списком измерений.
  • Для интеграционных тестов агенту нужны правила базы и подмены внешних сервисов в AGENTS.md.
  • Ожидание из кода, проверка мока и пустые утверждения — признаки теста, который ничего не ловит.
Отметка хранится только в вашем браузере
Хотите связаться с владельцем
компании напрямую?
Дмитрий Тарасов
Дмитрий Тарасов
СЕО

НАПИСАТЬ

Написать в MAX