Вы открываете CI-дашборд: 100% зелёных галочек. Покрытие — 85%. Copilot или Claude сгенерировали сотни тестов за минуту. Команда хлопает друг друга по плечу. А через неделю — баг на проде, который эти тесты обязаны были поймать. Знакомо?
Проблема глубже, чем кажется. AI-модели 2026 года (GPT-5, Claude 4 Sonnet, Gemini 3) умеют писать синтаксически правильные тесты. Но они не чувствуют контекста. Они не знают, какой кейс критичен для бизнеса, а какой — просто проформы ради. В результате мы получаем фальшивое покрытие: тесты проходят, но баги остаются.
В предыдущей статье «Почему AI-тесты не ловят баги: 5 антипаттернов Cursor, Copilot и Claude с примерами» я разобрал типовые ошибки. Сегодня пойдём дальше: я дам конкретную методику, как отличить полезный AI-тест от бесполезного, и покажу на живых Java-примерах, как не дать продакшену упасть из-за того, что зелёная галочка — ложная.
Сразу предупреждение: не верьте ни одному AI-генерированному тесту, пока не проверите его по чек-листу ниже. Даже если он написан красиво и покрытие показывает 100%.
Что такое ложноположительный тест (и почему вы его не замечаете)
Ложноположительный тест — это тест, который проходит зелёным, хотя проверяет ничего значимого, или его прохождение не гарантирует отсутствие бага. Пример из жизни: представьте, что вы тестируете код оплаты, а AI сгенерировал тест, который мокает весь сервис и проверяет только то, что метод вызван с правильными параметрами. Сам расчёт суммы никак не верифицирован. Тест зелёный — деньги уходят не туда.
В 2025 году Stack Overflow показал, что 34% разработчиков считают свои тесты бесполезными. С AI-генерацией этот процент, подозреваю, перевалил за 50. Но как это проверить?
Методика простая: каждый AI-тест нужно прогнать через пять фильтров. Если хотя бы один не пройден — тест подозрительный.
1 Тест проверяет реальную бизнес-логику?
AI часто генерирует тесты, которые проверяют инфраструктуру, а не логику. Например, проверяет, что REST-эндпоинт отвечает 200, но не смотрит на тело ответа. Это не тест — это smoke check.
// ❌ AI-сгенерированный тест: проверяет только статус
@Test void testCreateOrder_returns200() {
mockMvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"productId\":1,\"quantity\":2}"))
.andExpect(status().isOk());
}
// ✅ Правильный тест: проверяет, что заказ создан с корректной суммой
@Test void testCreateOrder_validOrder_calculatesTotal() {
var result = mockMvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"productId\":1,\"quantity\":2}"))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
var order = objectMapper.readValue(result, Order.class);
assertThat(order.getTotalPrice()).isEqualByComparingTo(BigDecimal.valueOf(199.98));
}
2 Есть ли assert, который может упасть?
assertTrue(true) — классика. Но AI умеет маскировать пустышки. Пример: проверка, что список не пуст, но без конкретной проверки содержимого.
// ❌ AI тест: проверяет только что список не пуст
@Test void testGetUsers_returnsNonEmptyList() {
var users = userService.getUsers();
assertFalse(users.isEmpty()); // может быть всегда true из-за мока
}
// ✅ Тест проверяет конкретное значение
@Test void testGetUsers_returnsActiveUsers() {
var users = userService.getUsers();
assertThat(users).hasSize(3);
assertThat(users.get(0).getStatus()).isEqualTo(UserStatus.ACTIVE);
}
3 Моки не переопределяют логику?
AI обожает мокать всё подряд, включая методы, которые должны быть протестированы. В результате тест проверяет моки, а не реальный код. Вот типичная ловушка:
// ❌ AI тест: мокает сервис, который тестируем
@Test void testCalculateDiscount_withMockService() {
var discountService = mock(DiscountService.class);
when(discountService.calculateDiscount(any())).thenReturn(10.0); // мок самого тестируемого метода!
var result = discountService.calculateDiscount(new Order(100));
assertEquals(10.0, result); // всегда пройдёт
}
// ✅ Правильный тест: использует реальный сервис, мокаем только внешние зависимости
@Test void testCalculateDiscount_realService() {
var pricingClient = mock(PricingClient.class);
when(pricingClient.fetchDiscount(anyLong())).thenReturn(5.0); // внешняя зависимость
var discountService = new DiscountService(pricingClient);
var discount = discountService.calculateDiscount(new Order(100));
assertEquals(5.0, discount); // проверяем, что discountService применил логику
}
4 Граничные условия покрыты?
Исследование VAKRA (2026) показало: AI-агенты отлично справляются со счастливым путём, но проваливаются на edge cases. Если тест не проверяет null, пустые коллекции, переполнение, максимальные значения — он неполноценен.
// ❌ AI тест: только happy path
@Test void testDivide_positiveNumbers() {
assertEquals(2, calculator.divide(10, 5));
}
// ✅ Тест с граничными условиями
@Test void testDivide_byZero_throwsException() {
assertThrows(ArithmeticException.class, () -> calculator.divide(10, 0));
}
@Test void testDivide_negativeDivisor() {
assertEquals(-2, calculator.divide(10, -5));
}
@Test void testDivide_largeNumbers() {
assertEquals(1, calculator.divide(Integer.MAX_VALUE, Integer.MAX_VALUE));
}
5 Тест можно ревьювить как продакшн-код?
Если тест выглядит как закорючка — непонятные стримы, несколько уровней вложенности моков, магия с reflection — скорее всего, он бесполезен. Читаемость теста — залог его ценности.
// ❌ AI тест: магия с PowerMock и reflection
@Test void testMagic() {
PowerMockito.mockStatic(SomeSingleton.class);
when(SomeSingleton.getInstance()).thenReturn(mock);
// 10 строк рефлексии
}
// ✅ Простой тест: ясно что и зачем
@Test void testSimple() {
var result = service.process(new Request("test", 1));
assertEquals("expected", result);
}
Пошаговый план: как не пропустить баг
Теперь, когда мы знаем критерии, давайте применим их на практике. Вот алгоритм действий для каждого AI-сгенерированного теста.
1 Прочитайте тест без IDE
Звучит странно? Распечатайте или просто прочитайте глазами. Если вы не можете за 30 секунд понять, что именно проверяется — тест плох. AI часто генерирует многословные цепочки методов, которые скрывают суть.
2 Замените assert на противоположный
Попробуйте изменить assert на заведомо неверный (assertEquals(0, something) вместо assertEquals(42, something)) и запустите тест. Если он всё ещё зелёный — у вас бесполезный тест. Бесполезный тест не упадёт даже если сломать логику.
3 Удалите один assert и проверьте покрытие
Метод mutation testing: внесите баг в исходный код (например, замените + на -) и посмотрите, упадёт ли тест. Если не упал — тест не защищает от этого типа бага. В Java это можно сделать с помощью плагина Pitest (PiTest).
# Запуск PiTest в Maven
mvn test-compile org.pitest:pitest-maven:mutationCoverage
PiTest подскажет, какие мутации выжили. Если тесты AI не убивают мутантов — они бесполезны, несмотря на 100% покрытие строк.
4 Проверьте, что тест не завязан на конкретные ID
AI часто хардкодит ID объектов из моков. В реальности ID могут быть случайными. Тест, который ожидает 42 при каждом запуске, будет зелёным только в прогоне с фиксированным моком. Полезный тест использует сгенерированные ID или явные константы из конфигурации.
Ошибка: AI-сгенерированный тест часто использует anyLong() в моках, но потом проверяет результат с фиксированным ID. Это маскирует проблему: в production ID создаётся реальной базой, а не моком.
Автоматизация проверки: добавьте правила в CI/CD
Ручное ревью каждого теста — утопия для больших проектов. Но можно автоматизировать выявление подозрительных паттернов. Например, добавить статический анализ (Checkstyle, PMD, или ещё лучше — SonarQube с правилами для тестов).
Вот несколько эвристик, которые можно закодить в custom rule:
- Запретить
assertTrue(true)иassertFalse(false). - Запретить моки на тестируемый класс (это антипаттерн).
- Требовать хотя бы один assert с конкретным значением на тест (не просто проверка на пустоту).
- Проверять, что в тесте нет catch всех исключений с пустым блоком.
В статье про AI-агенты в SDLC мы обсуждали, как зелёный дашборд перестаёт быть правдой. Аналогично — правила в CI не дадут пустым тестам пройти код-ревью.
Пример из жизни: AI-тест, который обманул всю команду
Пару месяцев назад один из моих проектов (микросервис на Java 23, Spring Boot 3.4) попал в прод с багом. У нас были AI-сгенерированные тесты для слоя сервисов. CI зелёный, покрытие 87%. Как баг проскочил?
AI сгенерировал тест для метода processPayment(), который мокал PaymentGateway и проверял, что метод вызван с теми же параметрами. Но сам gateway возвращал null в случае ошибки, и в production у нас упал NPE. Тест не проверял обработку null — только вызов метода.
// ❌ AI тест, который обманул:
@Test void testProcessPayment_success() {
when(paymentGateway.charge(any())).thenReturn(new PaymentResult("ok", "tx123"));
service.processPayment(100.0);
verify(paymentGateway).charge(new PaymentRequest(100.0)); // мок, не проверяет ответ
}
// ✅ Правильный тест, который поймал бы баг:
@Test void testProcessPayment_nullResponse_throwsException() {
when(paymentGateway.charge(any())).thenReturn(null);
assertThrows(PaymentProcessingException.class, () -> service.processPayment(100.0));
}
После этого инцидента мы ввели обязательное mutation testing для всех AI-тестов. Теперь каждый новый тест проходит PiTest. Если мутация выживает — тест не принимается. Результат: количество багов, пропущенных тестами, упало на 90%.
Когда AI-тесты действительно полезны?
Не хочу демонизировать AI. Есть сценарии, где сгенерированные тесты отлично работают:
- Регрессионное тестирование — AI может быстро создать тесты для API-контрактов (OpenAPI -> auto tests).
- Data-driven тесты — генерация множества наборов данных из JSON-схем.
- Smoke тесты инфраструктуры — проверка, что конфигурация поднимается.
Но для тестирования бизнес-логики, особенно с граничными условиями, AI пока слаб. Поэтому каждому AI-тесту нужно проходить через наш чек-лист — иначе это мусор.
Следующий шаг: как превратить AI-тесты в настоящую защиту?
Если вы хотите глубже разобраться, рекомендую методику построения бенчмарков для AI-агентов. Там мы показывали, как замерять реальную пользу от AI в тестировании и не переплачивать за иллюзию.
А если вы хотите научиться защищать свои AI-системы от вредоносных выходов, прочитайте гайд по Guardrails — он научит выстраивать барьеры для LLM.
И последнее: я не говорю, что AI в тестировании — зло. Я говорю, что без proper validation это опасное оружие. Используйте его с умом, и тогда зелёная галочка будет означать именно то, что должна: код корректен.