Есть особый сорт дефекта, который появляется часто тогда, когда код и тесты к нему пишет один и тот же агент. Это известная проблема, которую я уже несколько раз упоминал и даже писал, что Claude с ней разобрался самостоятельно. Проблема называется "тест, который не может упасть". Он зелёный, он в CI, он выглядит как проверка, но если сломать то, что он охраняет, он останется зелёным. И вы об этом не узнаете, потому что узнать об этом теперь можно только одним способом - известным в народе тестированием продом.
Claude после соответствующих моих докапываний поделился почему так: причина, говорит, в том, что тест он пишет из того же понимания, из того же контекста, что и код, впрочем, как и мы до появления хорошей насмотренности на то, какими тесты вообще должны быть. Если агент решил, что значение приходит вот отсюда, то и код напишет так, и проверять будет там же, подгоняя решение под ответ. Ошибка в понимании копируется в обе стороны, и они прекрасно между собой согласуются. Если тесты пишутся, исходя из требований, то они обычно получаются сильнее, чем тесты, написанные сразу во время написания логики, которую они же и должны тестировать. Агент, пишет и код и тесты, используя один и тот же контекст, если не разделять специально.
Первый раз я его поймал на истории, где оплаченное действие пользователя не сохранялось в базу вообще. В коде была перепутана пара типов при записи JSON, данные молча не доезжали. При этом юнит-тест на сохранение был, он был зелёный и он был бессмысленным - он сверял строку с фейковым коннектом, то есть проверял, что мок вернул то, что в мок положили. Ни одной строчки от настоящего пути в нём не было. Из этой ошибки выросло первое правило: проверять через тот же путь приложения, которым пользуется прод, а не через хелпер. Дёргай шаг целиком, ходи в настоящую базу, посылай настоящий запрос в приложение.
Помогло, но не спасло. Следующая история была тоньше. У меня есть отдельный гард - проверка, которая должна гарантировать, что определённый шаг пайплайна отдаёт результат в нужной форме. Тест был честный, ходил через настоящий вызов и был зелёным. Только считал он ответ сам, своей копией логики, а не тем, что реально вернул шаг. Шаг откатили на старую версию - тест не заметил. Он проверял намерение, а не факт. Отсюда второе правило, которое мы с Клодом заучили как мантру: гард должен проверять факт, а не намерение. Если ты в тесте пересчитываешь ожидаемый ответ той же формулой, что и код, ты сравниваешь формулу с самой собой. Абстрактный пример для лучшего понимания чего в тестах быть не должно:
expected = calculate_price(order)
assert response.price == expected
Дальше. Недавно я добавлял в проект роль "админ readonly" и обнаружил, что десяток тестов админских страниц всё это время проходили от лица анонимного гостя. Просто потому, что роль до этого дня ни на что не влияла, и разницы не было видно. Тесты были зелёные ровно потому, что проверять было нечего. Как только роль начала что-то значить, они посыпались все разом. Зелёный тест отвечал "работает" на вопрос, не имеющий смысла.
Так у нас появился ритуал, который зафиксирован отдельным правилом. Написал проверку - сломай то, что она охраняет, и посмотри, что она покраснела. Это позволяет отделить реальную проверку от её имитации. По сути, это ручное, но теперь руками агента, мутационное тестирование.
Но вы, надеюсь, не подумали, что на этом проблемы с тестами прекратились? Мутация, оказывается, тоже может не сработать. Буквально на этой неделе Claude сломал правило в файле, прогнал тесты и увидел в выводе "1 error". Обрадовался, что поймал - и чуть не пошёл исправлять. А это была не упавшая проверка, это питон не смог собрать файл. Клод неаккуратно вырезал строку и порвал синтаксис. То есть тесты вообще не запускались, и проверка проверки ничего не проверила.