Посвящается нашим любимым @ednersky , который стойко обороняет свою Перламутровую Башню из слоновой кости от нападок ржавых..., ржавых... терминаторов и @IUIUIUIUIUIUIUI, без устали занимающегося продвижением наших главных добродетелей: лени, гордыни и самомнения.
Вообще говоря, было бы неплохо обсудить тестирования с нашей точки зрения с учётом изменившегося ландшафта, то есть LLM (больших языковых моделей).
Как известно, многие используют нейросетки для написания unit тестов. Типичная фраза: «я написал код, а потом ChatGPT мне за несколько секунд добавил тесты». Или «код писать ИИ я не доверяю, но тесты он пишет хорошо». Если честно, меня, когда я это читал, не оставляло ощущение какой-то неправильности, однако выводить в рацио было, право слово, лень.
Но сейчас я бодр, что-то полезное делать категорически не желаю, поэтому приступим!
Unit тесты бывают разных типов, не только тесты свойств (property-tests, QuickCheck, etc). Там, вы представляете, коллеги, во мраке и ужасах, во владениях жуткого холоднокровного, которое душит и глотает, люди пишут тесты вручную, на, извините за выражение, assert
'ах. И эти несчастные, вынужденные работать день и ночь в поте лица своего, разумеется, хотят хоть немного облегчить свою участь. Как известно, в подобной ситуации Резерфорд помрачнел и раздраженно спросил: — Послушайте, а когда же вы думаете?
Но у нас время есть, поэтому за них подумаем мы. Итак, какие вообще есть цели у unit-тестов, написанных на assert'ах? Главная, разумеется, это ИБД, чтобы не выгнали из гестапо за жестокость, а побочных две:
Фиксация текущего кода, ну чтобы кто-то с пониженной ответственностью перед социумом пока печатает не приходя в сознание не внёс очевидных ошибок.
Проверка кода при написании.
Разумеется, в отличие от тестов свойств, являющихся прогонами продвинутого Монте-Карло, тесты на assert
'ах проверяют лишь небольшое количество точек конфигурационного пространства.
Как несложно вспомнить из курса матана, ну тем, конечно, у кого он был, для проверки линейной функции одного агумента нужно взять две точки; для проверки квадратичной — три, и так далее. Количество точек не то, чтобы быстро, но растёт с увеличением сложности функции и размерности конфигурационного пространства. Уже для квадратичной функции двух переменных нужно 6 точек, а ведь эта функция преспокойно разместится на двух строках. Страшно представить, сколько точечных тестов нужно для типичной простыни на два экрана из кровавого ынтерпрайза и типичным набором из 6-7 аргументов.
Поэтому, первая из побочных целей написания unit-тестов, разумеется не достигается. Но, безусловно, продолжительный труд и большие объёмы бессвязных текстов утешают и дают надежду.
Про вторую из побочных целей при условии использования LLM, и заикаться-то странно, как мы понимаем. Ведь если unit-тест может написать автоматика, которая из всего контекста имеет лишь код тестируемой функции, то всё, что она может сделать — это переписать исходный текст в немного изменённом виде. Поэтому если мы написали функцию, складывающую 2 и 3, при этом получающую 6, то LLM может написать, что 3 + 2 = 6 или, что 6 - 3 = 2.
И дело тут, разумеется, не в недоразвитости LLM, нет, дело в контексте, который у неё есть. Ну не родятся от осины золотые апельсины. Чтобы что-то проверить, нужна избыточная информация помимо уже написанного кода. Когда кодер пишет тест для своего текста на assert
'ах вручную, он лишь имеет небольшой шанс её добавить. Поэтому здравомыслящие люди, работающие в поте лица своего с использованием C в embedded, давно уяснили, что писать тестируемый код должен один программист, а тесты к нему — другой.
Обычные же для функционального программирования тесты свойств лишены этого недостатка. Они пишутся на декларативном eDSL, поэтому хочешь, не хочешь, а придётся добавлять избыточную информацию — ведь напрямую извлекать её из функционального кода сложнее, чем из своей модели происходящего. В общем, только самые трудолюбивые из нас могут писать тривиальные тесты свойств погонными метрами.