Когда все тесты зелёные, а баги едут в прод
- среда, 2 сентября 2026 г. в 00:00:14
Статья подготовлена в рамках курса «Автоматизатор тестирования на 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. Они проходят для любого непустого значения, даже если оно неправильное.
Исследователи выделяют целый класс антипаттернов:
Assertion Roulette (несколько ассертов без сообщений, сложно понять, какой упал);
Duplicate Assert (повторяющиеся проверки);
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 собрали в дайджесте.