javascript

Когда все тесты зелёные, а баги едут в прод

  • среда, 2 сентября 2026 г. в 00:00:14
https://habr.com/ru/companies/otus/articles/1073708/

Статья подготовлена в рамках курса «Автоматизатор тестирования на JavaScript».

Привет, Хабр!

Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.

Вы когда‑нибудь получали от ИИ тесты, которые «идеально проходят», а через неделю ловили баг в том самом месте, которое должно быть покрыто?

Это не случайность — это симптом тавтологического тестирования.

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

Допустим, ставка налога — 10%. Но разработчик случайно написал price * 0.08. Вы просите ИИ написать тест. ИИ читает код, видит 0.08 и генерирует:

test('calculateTax returns correct tax', () => {
  expect(calculateTax(100)).toBe(8);
});

В итоге, тест зелёный, покрытие 100%, и баг улетает в прод. Это и есть тавтологический тест — проверка, которая просто переписывает логику реализации на языке тестов и никогда не может упасть, даже если реализация ошибочна. Коварство в том, что такие тесты создают иллюзию качества.

Покрытие растёт, CI зелёный, а реальные регрессии остаются незамеченными.

Вот один из ярких реальных примеров: разработчик попросил Copilot сгенерировать тесты для React‑хука с дебаунсом (кастомным хуком, который помогает оптимизировать обработку частых событий, откладывая выполнение функции до тех пор, пока не пройдёт заданная задержка после последнего вызова).

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

Исследования показывают, что эта проблема массовая: при 100% покрытии строк мутационный скор может быть всего 4%. Это значит, что 96% возможных багов остаются незамеченными. Более того, исследования CodeRabbit показывают, что AI‑сгенерированный код вносит в 1.7 раз больше багов, чем человеческий. И ИИ‑тесты часто не ловят эти баги, потому что они построены по принципу «поверь мне на слово».

Как ИИ попадает в ловушку: главные причины

Основной причиной того, что искусственный интеллект попадает в подобную ловушку, является то, что ему доступна реализация.

Современные инструменты часто передают в LLM исходный код вместе с запросом на генерацию тестов. Модель «умная»: она видит, что функция делает, и пишет тест, который это повторяет. Это не проверка — это пересказ.

Исследования подтверждают: когда ИИ видит реализацию при генерации тестов, качество тестов резко падает. В одном из экспериментов точность тестов выросла с 61% до 87,8%, когда ИИ перестали показывать реализацию.

Также, важная причина — это то, что ИИ любит мокать (в смысле mock) что называется все подряд.

Исследование 2025 года показало: в AI‑сгенерированных тестах доля моков составляет 36% против 26% у человеческих. При этом 95% моков — это стандартные заглушки, почти без spy или проверок реального поведения.

test('calculateTotal sums line items', () => {
  const calc = jest.spyOn(cart, 'calculateTotal').mockReturnValue(42);
  expect(cart.calculateTotal()).toBe(42);   // тестирует мок, а не код
});

Этот тест всегда зелёный, независимо от того, правильная логика в calculateTotal или нет. Он проверяет, что мок вернул то, что вы ему сказали вернуть.

И еще одна причина заключается в том, что ИИ пишет слабые ассерты.

Самые частые проблемы в AI‑тестах — слабые проверки вроде toBeDefined(), not.toBeNull() или length > 0. Они проходят для любого непустого значения, даже если оно неправильное.

Исследователи выделяют целый класс антипаттернов:

  1. Assertion Roulette (несколько ассертов без сообщений, сложно понять, какой упал);

  2. Duplicate Assert (повторяющиеся проверки);

  3. Generic Naming (“should work correctly” вместо конкретного поведения).

Что делать: стратегии с примерами

Самый эффективный способ борьбы с тавтологическими тестами — архитектурно запретить ИИ‑ассистенту видеть реализацию при генерации тестов. Например, в Claude Code можно создать саб‑агента test-generator с ограниченными правами:

#.claude/agents/test-generator.md
tools: Write, Bash  # ← Read отсутствует!

Такой подход физически запрещает агенту открывать файлы с кодом. Вместо этого он получает только спецификацию в формате Given‑When‑Then:

describe("processOrder", () => {
  test("applies 10% discount for orders over $100", () => {
    // Given: корзина с товарами на $120
    const cart = { items: [{ price: 120, qty: 1 }] };
 
    // When: оформляем заказ
    const result = processOrder(cart);
    
    // Then: применена скидка 10%
    expect(result.total).toBe(108);
  });
});

В такой постановке ИИ не может «подглядеть» и написать expect(result.total).toBe(120 * 0.9), потому что он не видел реализацию. Он работает только с описанием ожидаемого поведения.

Эксперименты показывают, что ограничение доступа к реализации снижает «читерство» ИИ практически до нуля.

Еще одна интересная стратегия предлагает использовать verify‑цикл с auto‑fix. Вместо того чтобы верить первому варианту тестов, можно заставить ИИ запускать их и исправлять до зелёного статуса.

Пример с testpilot‑ai:

npx testpilot src/utils.ts --verify 

Инструмент делает следующее:

  • Анализирует код через TypeScript Compiler API — извлекает функции, JSDoc‑аннотации и типы.

  • Генерирует тесты.

  • Запускает их.

  • Если тесты упали, передаёт ошибки обратно в LLM с контекстом: «вот исходный код, вот упавший тест, вот ошибка — исправь».

  • Повторяет до 3 итераций, отслеживая регрессии и останавливаясь при повторяющихся ошибках.

В реальном примере testpilot сгенерировал 12 тестов для утилиты.

  • Первая итерация: 3 теста упали. ИИ получил стектрейсы, исправил тесты.

  • Вторая итерация: все 12 прошли.

Здесь стоит обратить внимание на важный нюанс. testpilot использует AST‑анализ перед LLM, а не просто передаёт сырой код. Это даёт модели структурированную информацию о типах и сигнатурах, не заставляя её «догадываться».

Также, вместо конкретного примера («при цене 100 налог должен быть 8») проверяйте свойства, которые должны выполняться для любых входных данных. Тогда тавтология становится невозможной — ИИ не может подогнать ожидание под реализацию, потому что свойство универсально.

Пример с библиотекой fast‑check:

import fc from 'fast-check';

test('calculateTax should never exceed price', () => {
  fc.assert(
    fc.property(fc.float({ min: 0, max: 10000 }), (price) => {
      const tax = calculateTax(price);
      // Свойство: налог не может быть больше цены
      expect(tax).toBeLessThanOrEqual(price);
      // Свойство: налог не может быть отрицательным
      expect(tax).toBeGreaterThanOrEqual(0);
    })
  );
});

Даже если в реализации price 0.08 вместо price 0.1, тест не угадает ожидание. Он проверит универсальное свойство: налог не превышает цену. Для цены 100 и ставке 8% свойство выполняется, но для проверки корректности ставки понадобится другое свойство — например, что для двух одинаковых цен налог одинаков.

Проверяем тесты на «мутантов»

Mutation testing — это проверка тестов на прочность. Здесь мы вносим небольшие изменения в код («мутантов») и смотрим, упал ли тест. Если тест зелёный после изменения — он бесполезен.

Пример с TestMutant:

npx @phoenixaihub/test-mutant scan./src

Инструмент сканирует тесты на антипаттерны:

  • Tautological Assertions — тесты, которые проходят независимо от реализации

  • Mock‑Everything — больше 80% зависимостей замокано

  • No Edge Cases — только счастливые пути

  • Generic Naming — “should work correctly” вместо конкретики

Каждый файл получает Integrity Score от 0 до 100. В CI можно поставить порог:

test-mutant ci./src --threshold 70  # упадёт, если качество тестов ниже 70

Как внедрять в проект:

  • Начните с одного модуля. Выберите критичную бизнес‑логику, где баги особенно дороги. Не пытайтесь переписать все тесты сразу.

  • Настройте агента без доступа к коду. Если используете Claude Code, создайте саб‑агента test-generator без Read‑инструментов. Используйте Given‑When‑Then как входной формат.

  • Подключите verify‑цикл. Для нового кода используйте testpilot src/module.ts --verify. Это автоматически проверит, что сгенерированные тесты действительно проходят и не являются «пустышками».

  • Добавьте property‑based тесты для ключевых алгоритмов. Начните с простых свойств: «результат всегда положительный», «сумма элементов массива равна исходному значению».

  • Внедрите проверку качества тестов в CI. Добавьте test-mutant ci./src --threshold 60 в pre‑commit hook или GitHub Actions. Если скор падает ниже порога — CI красный, тесты надо переписывать.

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

Подведём итоги

Тесты, написанные ИИ, опасны не тем, что они падают, а тем, что они не падают когда надо. Зелёный CI — это не гарантия качества, если тесты переписывают реализацию, мокают всё подряд или используют слабые ассерты.

Современные инструменты уже умеют решать эту проблему: ограничивать доступ ИИ к коду, запускать verify‑циклы, проверять свойства и анализировать качество самих тестов. Главное — не доверять AI‑тестам без проверки. Требуйте от ИИ не просто «написать тесты», а доказать, что они действительно ловят баги.

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

На бесплатных уроках разберем, как это работает на практике:

  • 2 сентября в 20:00. «ИИ для тестировщика: инструменты, которые уже меняют профессию». Записаться

  • 22 сентября в 20:00. «Playwright JS: как быстро начать писать автотесты?». Записаться

Полный список ближайших открытых уроков от преподавателей Otus собрали в дайджесте.