# Выявляет ли мутационное тестирование кода, созданного ИИ, пустые тесты?

> Мутационное тестирование кода, созданного ИИ, выявляет тесты, которые проходят, пока биллинг, права доступа и правила валидации незаметно ломаются.

Код, созданный ИИ, изменил характер проблем с тестированием. Раньше команды опасались, что новый код появится без тестов. Теперь coding agent может за один pull request создать реализацию, моки, фикстуры и обнадёживающий зелёный прогон тестов. Опасный вариант такого pull request выглядит завершённым: высокое покрытие строк, аккуратные названия тестов и проверки, которые ничего не доказывают о правилах, заметных пользователю.

Мутационное тестирование даёт этим тестам противника. Вы вносите небольшую ошибку в реализацию и проверяете, заметит ли её набор тестов. Если меняется итог счёта, переворачивается проверка прав доступа или обязательное поле становится необязательным, а все тесты остаются зелёными, значит, тесты были написаны, но поведение не защищали.

Это не призыв запускать тысячи случайных мутаций для каждого репозитория. Так вы потратите время CI и приучите людей игнорировать отчёт. Применяйте метод там, где ИИ способен усилить знакомую инженерную ошибку: он читает реализацию, затем пишет тесты и повторяет логику реализации в другом файле.

## Прошедший сгенерированный тест всё ещё может ничего не защищать

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

Рассмотрим сгенерированную функцию расчёта счёта:

```ts
type Invoice = {
  subtotalCents: number;
  taxRate: number;
  discountCents: number;
};

export function totalDueCents(invoice: Invoice): number {
  const discounted = Math.max(0, invoice.subtotalCents - invoice.discountCents);
  return Math.round(discounted * (1 + invoice.taxRate));
}
```

Агент может создать такой тест:

```ts
it("calculates the invoice total", () => {
  const invoice = { subtotalCents: 10_000, taxRate: 0.08, discountCents: 1_000 };

  const expected = Math.round(
    Math.max(0, invoice.subtotalCents - invoice.discountCents) * (1 + invoice.taxRate)
  );

  expect(totalDueCents(invoice)).toBe(expected);
});
```

Тест выполняет каждую строку. Но он также дублирует формулу. Если изменить рабочий код так, чтобы налог начислялся на сумму до скидки, а затем внести такое же изменение в выражение `expected` в тесте, набор тестов останется зелёным. Тест превратился во вторую реализацию, а не в независимую проверку.

Поведенческий тест называет результат, который может распознать клиент, бухгалтер или специалист поддержки:

```ts
it("does not charge tax on the discounted portion", () => {
  const invoice = { subtotalCents: 10_000, taxRate: 0.08, discountCents: 1_000 };

  expect(totalDueCents(invoice)).toBe(9_720);
});
```

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

Покрытие строк отвечает на вопрос: «Выполнил ли тест эту инструкцию?» Мутационное тестирование спрашивает: «Завершился бы тест ошибкой, если бы эта инструкция делала что-то неправильно?» Это разные вопросы. Набор тестов может заявлять о 100% покрытия строк и при этом пережить перевёрнутое условие авторизации или пропущенную ветку валидации.

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

## Мутируйте решения, которые меняют деньги, доступ и данные

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

Полезный вопрос звучит не так: «У чего низкое покрытие?» Спросите: «Какая ошибка в одну строку создаст обращение в поддержку, инцидент безопасности или неверную финансовую запись?» ИИ увеличивает объём поставки, поэтому системе проверки нужно сосредоточиться на точках принятия решений, а не поощрять рост числа файлов с тестами.

Для логики биллинга можно внедрить такие ошибки:

- начислить налог до скидки, а не после неё;
- заменить `>` на `>=` на ценовом пороге;
- позволить отрицательной корректировке увеличить возврат;
- округлять каждую позицию, а не итоговую сумму;
- пропустить проверку валюты или статуса счёта.

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

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

Хорошая цель для мутации достаточно мала, чтобы инженер мог одним предложением объяснить ожидаемое поведение. Если никто не может сформулировать это предложение, мутационное тестирование выявит более основную проблему: у команды есть код, но нет согласованного правила.

## Мутации биллинга выявляют арифметические тесты, которые лишь создают видимость работы

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

Для правил, которые должны давать известную сумму, используйте фиксированные примеры. Для зависимостей, которые должны сохраняться на множестве входных данных, применяйте property-based тесты. Используйте оба подхода только там, где правило оправдывает дополнительные усилия.

Возьмём правило перехода на другой тариф. Клиент переходит с месячного плана за $100 на план за $160 в середине 30-дневного периода. Допустим, по договору кредит рассчитывается на основе неиспользованного времени старого плана, а списание, на основе оставшегося времени нового плана. Конкретный тест должен явно указать ожидаемую чистую сумму. Не следует вычислять её, вызывая тот же помощник для пропорционального расчёта или воспроизводя его ветвления.

Затем внедрите мутации, похожие на настоящие финансовые ошибки:

```ts
// Intended check
if (creditCents > remainingChargeCents) {
  return { amountDueCents: 0, carryForwardCents: creditCents - remainingChargeCents };
}

// Useful mutant
if (creditCents >= remainingChargeCents) {
  return { amountDueCents: 0, carryForwardCents: creditCents - remainingChargeCents };
}
```

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

Другая мутация убирает нижнюю границу скидки:

```ts
const discounted = invoice.subtotalCents - invoice.discountCents;
```

Если тесты допускают отрицательную итоговую сумму при слишком большой скидке, вы узнали больше, чем просто то, что «мутация выжила». Вы обнаружили решение, которое продукт ещё не принял. Должна ли система отклонить счёт, ограничить сумму нулём или начислить кредит на счёт? Тест не ответит на этот вопрос. Ответ должен дать владелец продукта или финансового процесса.

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

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

## Тесты прав доступа должны доказывать отказ, а не только успех

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

Такой тест доказывает, что заглушка умеет вернуть true.

Разместите правила авторизации в policy-функции или на узкой границе, где их можно проверить с реальными идентификаторами и атрибутами ресурса. Затем примените мутацию, расширяющую доступ. Если исходная политика выглядит так:

```ts
export function canEditProject(actor: Actor, project: Project): boolean {
  return actor.organizationId === project.organizationId && actor.role === "admin";
}
```

то наиболее ценные мутации будут вполне обычными. Именно такие сокращения люди делают под давлением сроков:

```ts
return actor.organizationId === project.organizationId || actor.role === "admin";
```

```ts
return actor.role === "admin";
```

```ts
return actor.organizationId === project.organizationId && actor.role !== "viewer";
```

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

Не подгоняйте тест под строки ролей в реализации. Тест прав доступа должен проверять разрешённую или запрещённую операцию. Если HTTP-эндпоинт меняет вспомогательную функцию политики, тест всё равно должен показать, удалось ли изменить проект между арендаторами. Поэтому один интеграционный тест на реальном маршруте может быть ценнее десяти модульных тестов, проверяющих внутренние вызовы политики.

Тест также должен проверить, что запрещённый запрос не оставил следов. Если эндпоинт возвращает 403 после постановки экспорта в очередь, обновления записи или отправки события, проверка прав есть, но расположена не там. Мутационное тестирование может помочь это обнаружить, переместив или удалив защиту, но проверка должна осмотреть изменение состояния.

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

## Валидация должна находиться на границе поступления некорректных данных

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

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

Представим, например, эндпоинт создания выплаты:

```ts
export type CreatePayoutRequest = {
  accountId: string;
  amountCents: number;
  currency: "USD" | "EUR";
};
```

Правило безопасности может включать больше, чем проверку типов. `amountCents` должно быть положительным целым числом, `accountId` должен принадлежать организации вызывающего пользователя, а целевой счёт должен поддерживать выбранную валюту. Агент может создать множество тестов для пустых строк и отсутствующих JSON-полей, но не проверить владение счётом.

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

```ts
it("rejects a payout account owned by another organization", async () => {
  const response = await requestAs(orgAAdmin).post("/payouts").send({
    accountId: orgBAccount.id,
    amountCents: 5_000,
    currency: "USD"
  });

  expect(response.status).toBe(403);
  expect(await payoutRepository.count()).toBe(0);
});
```

Теперь удалите сравнение организаций из валидатора или обработчика. Тест должен завершиться ошибкой. Если этого не произошло, не добавляйте общее утверждение `expect(response.body).toBeDefined()` и не переходите дальше. Проследите путь запроса и выясните, какая зависимость, фикстура или слишком широкий мок скрыло правило.

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

## Мутационный показатель это сигнал для расстановки приоритетов, а не цель

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

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

Используйте отчёт, чтобы создать короткую очередь решений:

1. Прочитайте diff выжившей мутации до теста. Решите, важно ли изменённое поведение.
2. Если оно важно, добавьте или исправьте минимальный тест, который должен завершиться ошибкой при таком поведении.
3. Если наблюдаемое поведение не меняется, пометьте мутацию как эквивалентную или исключите этот оператор с письменным объяснением.
4. Если мутация выявляет неясное требование, перестаньте считать это задачей по тестированию и получите решение от продукта.
5. Если тест медленный или нестабильный, сначала исправьте границу теста и только потом увеличивайте бюджет мутаций.

Эквивалентные мутации реальны. Замена `x + 0` на `x - 0` не меняет поведение. Условие в коде, защищённом инвариантом, тоже может оказаться эквивалентным в пределах достижимых состояний программы. Не обманывайте инструмент широким правилом игнорирования. Укажите причину рядом с исключением, потому что инвариант, действующий сегодня, может исчезнуть при следующем рефакторинге.

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

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

## Запускайте мутации, пока проверка ещё может изменить код

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

Запускайте быстрые целевые мутации для изменённых файлов с бизнес-правилами и их непосредственных тестов во время pull request. Более широкие задания запускайте по расписанию или перед релизом, когда репозиторий может позволить такие вычисления. Прикрепляйте отчёт к изменению, которое добавило логику, чтобы автор увидел конкретное утверждение вроде «замена `>` на `>=` в проверке права на пробный период не завершила ни один тест ошибкой».

Рабочая политика для стартап-команды может выглядеть так:

- Мутируйте изменённый код биллинга, прав доступа, валидации и переходов состояния аккаунта в каждом pull request.
- Завершайте pull request ошибкой только из-за новых выживших в этой ограниченной области после разрешённого документированного подавления эквивалентной мутации.
- Запускайте более широкую проверку ночью или перед созданием релизной ветки, а затем назначайте только те выжившие мутации, которые затрагивают известные бизнес-правила.
- На начальном этапе исключите сгенерированные клиенты, миграции, связующий код фреймворка и простое отображение данных.
- Отслеживайте время выполнения и нестабильные тесты. Отчёт, которому никто не доверяет, не улучшит тесты.

Не позволяйте агенту запускать неограниченное мутационное тестирование, а затем автоматически добавлять десятки новых тестов. Такой подход создаёт объём, но не уверенность. Давайте агенту по одной выжившей мутации, правило, которое она нарушает, и границу теста, где должно проверяться поведение.

Запрос с ограничениями даст результат лучше, чем «улучши тесты»:

```text
The mutation changed `actor.organizationId === project.organizationId`
to `true`, and all tests passed.

Add one integration test for PATCH /projects/:id that uses an administrator
from a different organization. Assert HTTP 403 and assert that the project
name is unchanged. Do not mock the authorization policy. Do not change
production code.
```

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

## Сгенерированным тестам нужен независимый источник истины

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

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

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

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

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

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

Если ваша команда выпускает изменения, созданные ИИ, быстрее, чем успевает оценивать поведенческие риски, Team & AI Audit поможет определить, где тесты, CI и правила ответственности пропускают слабые изменения.
