Разбираем, как AI помогает писать тесты, а где создаёт иллюзию проверки. Узнайте, как отличить полезную автоматизацию от пустого покрытия и не пропустить баги в прод.
Вы только что закончили функцию, скопировали её в AI-помощник и попросили написать тесты. Через тридцать секунд у вас двенадцать тестов. Они запускаются, проходят, coverage подрос на процент. Вы чувствуете, что сделали что-то полезное. В большинстве дней так и есть.
Но потом в проде падает баг, вы пролистываете те двенадцать тестов и понимаете: ни один из них не поймал бы эту ошибку. Некоторые не могли — они тестировали реализацию, а не поведение. Один мокал именно то, что должен был проверить. Badge покрытия говорил правду: о покрытии, а не о корректности.
Эта статья — часть истории про AI для тестирования, которая не влезает в маркетинговые слайды. AI реально полезен для генерации тестов. Но он также отлично умеет создавать тесты, которые выглядят как тесты, но на самом деле ничего не проверяют. Всё искусство — понять, что именно вы только что получили.
Не будем циниками. Есть настоящий, устойчивый выигрыш в продуктивности от использования AI для написания тестов. Просто он уже, чем обещают маркетологи.
AI отлично экстраполирует по чёткому примеру. Если вы уже написали один хороший тест, который отражает контракт, попросите AI сгенерировать ещё десять в том же духе — это сработает почти всегда. Он подхватит ваш стиль утверждений, фабричные функции, соглашения об именах и за секунды выдаст стену правдоподобных вариантов. Десятый граничный случай — это именно та работа, которую опытный инженер не любит делать вручную, а AI справляется с ней на ура.
AI также хорош в частях тестового файла, которые в основном состоят из набора кода: блоки setup, teardown, фабричные хелперы, таблицы параметризованных входных данных, строители моков для уже определённых форм. Часовая ставка сеньора, потраченная на сороковой раз набор const user = { id: 'u_1', email: 'a@b.com', ... } — один из самых лёгких способов сэкономить в разработке, и AI сводит эти затраты к нулю.
И он разумный каркас для нового кода. Если вы написали функцию и хотите стартовую точку (файл с правильными импортами, правильным блоком describe, тремя-четырьмя скелетными тестами с утверждениями, оставленными как TODO), AI доставит вас до редактора быстрее, чем начать с пустого файла.
Заметьте, что общего у этих сценариев. В каждом случае вы всё ещё задаёте структуру, которая определяет, что значит «правильно». AI заполняет тело контракта, который написали вы. Это продуктивный режим. Генерация тестов сходит с рельсов в тот момент, когда вы переворачиваете отношение: когда просите AI решить, каков контракт, а не просто набрать его текст.
Дайте AI функцию и попросите модульные тесты. Посмотрите, что он сделает.
Он читает тело вашей функции. Замечает ветвления. Затем пишет один тест на каждую ветку. Результат выглядит полным: каждый if, каждый else, каждый ранний возврат имеет соответствующий блок it(). Покрытие достигает 100%. Все довольны.
Проблема в том, что AI не тестировал, что функция должна делать. Он тестировал то, что функция сейчас делает. Это разные артефакты, и разница — там, где живут баги.
Рассмотрим функцию скидки. Вы отправляете что-то вроде:
def apply_discount(amount: float, code: str) -> float:
if code == 'SUMMER25':
return amount * 0.75
if code == 'FRIEND10':
return amount * 0.9
return amountВы просите AI написать модульные тесты. Он даёт:
def test_apply_discount():
assert apply_discount(100, 'SUMMER25') == 75
assert apply_discount(100, 'FRIEND10') == 90
assert apply_discount(100, 'NOPE') == 100Эти тесты проходят. Функция «протестирована». И если вы случайно измените множитель SUMMER25 с 0.75 на 0.6, ровно один из них упадёт, сообщая вам, что код делает то, что делает, но никогда не скажет, что код делает то, что нужно бизнесу.
Теперь посмотрите на контракт, который вам на самом деле нужно было проверить:
code_unrecognized.Ни одно из этих условий не живёт в тестовом файле AI. Они и не могли. AI их никогда не видел. Он видел тело функции и вывел контракт из формы кода, а это как раз неверное направление. Контракт должен порождать код, а не наоборот.
Вот тонкая причина, почему «AI пишет тесты для функции, которую AI только что написал» — такой заманчивый и бесполезный ход. AI пишет функцию в рамках неявного предположения (скажем, что скидки всегда положительны), а затем пишет тесты в рамках того же неявного предположения. Скрытая предпосылка сидит внутри обоих артефактов, соглашаясь сама с собой. Продакшн — первое место, где эта предпосылка оспаривается, и в этот момент набор тестов оказывается на стороне бага.
Исправление не в том, чтобы перестать использовать AI для модульных тестов. А в том, чтобы перестать позволять AI решать, что проверяет тест. Напишите контракт первым, по одному предложению на тест, простым языком: «неизвестные коды возвращают исходную сумму», «просроченные коды никогда не применяются, даже если они введены правильно». Затем передайте эти предложения AI и позвольте ему заполнить утверждения. AI разрешено генерировать тело теста. Ему не разрешено генерировать намерение теста.
Попросите AI о граничных случаях, и он с уверенностью их выдаст. Вот список, который он надёжно сгенерирует для почти любой функции:
null и undefinedКаждый из них — реальный граничный случай. Каждый из них также очевидный граничный случай, который вы найдёте на первой странице любой статьи «как писать лучшие тесты». Они полезны, они должны быть покрыты, и вас не должно впечатлять, что AI о них подумал.
Баги, которые болят в проде, почти никогда не приходят из очевидных границ. Они приходят из предметных границ. Вот неполный список предметных границ, которые AI не сгенерирует сам, в порядке возрастания «да, это был инцидент в проде»:
Ни один из этих случаев нельзя найти, читая тело функции. Все они требуют знания системы. AI можно о них рассказать (скормите ему предметные правила, и он чисто сгенерирует тесты), но он никогда не сгенерирует их сам, потому что не может. Он не знает историю ваших багов с часовыми поясами, ваших quirks collation, вашей семантики повторных попыток или неписаного инварианта, который никто не задокументировал, потому что все, кто был в комнате, когда его приняли, уже ушли.
Что это означает для ежедневной практики. Когда вы просите AI о «граничных случаях для этой функции», вы получите очевидные. Возьмите их, они бесплатны. Но затем посидите минуту и запишите от руки один реальный предметный граничный случай для тестируемой функции. Всего один. Честный список «вещей, которые эта функция должна обрабатывать и которые беспокоили бы меня в 3 часа ночи» короток, но именно этот список ловит баги, которые будят вас по ночам.
Есть привычка, которую можно выработать. Каждый раз, заканчивая функцию, набрасывайте однострочный комментарий «чего я боюсь» где-нибудь: в блокноте, в черновике описания PR, в строке # TODO: тесты. Не фильтруйте. Затем скормите эти строки AI как промпт для тестов. Вы не просите AI придумать граничные случаи; вы просите его написать утверждения для граничных случаев, которые вы уже придумали. Модель гораздо лучше справляется со второй задачей.
Это худшая из трёх, потому что самая тонкая и самая любимая. AI обожает мокать. Всё, что имеет побочный эффект (вызов базы данных, HTTP-запрос, часы, источник случайности, файловая система, шина сообщений), мокается по умолчанию. Причина разумна: тесты должны быть детерминированными, зависимости должны быть изолированы, и моки — канонический способ сделать и то, и другое. AI следует хорошо документированному паттерну.
Паттерн ломается двумя конкретными способами, и оба случаются достаточно часто, чтобы их можно было предсказать.
Первый провал: мок неверен, но тест всё равно проходит.
Вы тестируете функцию, которая вызывает внешний API. AI мокает API и возвращает фикстуру. Фикстура имеет форму, которую AI считает ответом API — то есть то, как выглядел пример в интернете, а не то, что возвращает ваш конкретный провайдер. Ваша функция читает response.data.success; реальный API возвращает response.body.ok; тест никогда этого не замечает, потому что мок был построен из предположений вашей функции, а не из реального вызова.
# Неправильный мок, сгенерированный AI
from unittest.mock import patch
@patch('module.stripe.charge')
def test_charge_success(mock_charge):
mock_charge.return_value = {'data': {'success': True, 'id': 'ch_123'}}
result = charge_customer(amount=1000, customer_id='c_1')
assert result == 'ch_123'Этот тест проходит. Он будет проходить вечно. И он неверен так, что вы не можете этого обнаружить изнутри тестового файла, потому что единственное, что он проверяет — что функция читает из вымышленной формы, которую придумал AI. Реальный провайдер может вернуть { success: false, error: 'card_declined' } в любом количестве граничных случаев, и этот тест ничего не скажет вам о том, обрабатывает ли ваш код их.
Моки — это нормально. Слепые моки — проблема. Мокайте на границе системы, используя записанный реальный ответ, или держите хотя бы один сквозной тест, который использует реальный клиент против песочницы. Если AI мокает зависимость, фикстура мока должна быть взята из реального захваченного ответа, а не из догадки AI о форме.
Второй провал: мок замораживает реализацию.
У вас есть функция, которая оркестрирует работу нескольких внутренних сервисов. AI мокает их все и утверждает, что каждый был вызван определённым образом:
# Плохо: тест привязан к внутренним вызовам
@patch('module.inventory.reserve')
@patch('module.payment.charge')
def test_place_order(mock_charge, mock_reserve):
place_order(...)
mock_reserve.assert_called_once_with({'product_id': 'p1', 'quantity': 1})
mock_charge.assert_called_once_with({'amount': 100, 'customer_id': 'c1'})Этот тест проверяет не то, что заказ размещён, а то, что были вызваны определённые функции с определёнными аргументами. Если вы рефакторите код и объединяете вызовы или меняете порядок, тест падает, даже если поведение осталось корректным. Теперь тест — это обуза, а не страховка. AI, будучи обучен на коде, где моки — стандарт, будет генерировать такие тесты по умолчанию. Вам нужно явно попросить его тестировать по контракту, а не по вызовам: «проверь, что заказ сохранён в базе с правильным статусом», а не «проверь, что вызван inventory.reserve». Это тонкое, но критическое различие.
AI — мощный инструмент для написания тестов, но только если вы остаётесь главным. Вот три правила, которые превратят AI из генератора иллюзий в настоящего помощника:
Следуя этим правилам, вы получите от AI настоящую продуктивность, а не просто увеличение покрытия. Покрытие — это метрика, а не цель. Цель — корректность. И её не делегируешь.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →