Есть две категории программистов. Первая пишет тесты, вторая работает. Шутейка, конечно, на троечку, но в каждой байке, застрявшей в пабликах мёртвых заархивированных форумов, под пылью и нафталином, — можно нащупать слой гранита настоящей правды. Модное ныне «покрытие кода тестами» напоминает попытку оклеить айсберг новогодней мишурой — вроде и весело, но Титаник все равно пойдет на дно.
Я собираюсь рассказать о том, как правильно тестировать код в изоляции (интеграционные тесты — зверь из соседнего вольера, и о нем — в другой раз). Для этого нам потребуется пара определений. Фаззинг (от английского fuzzing) — это способ тестирования, при котором программе скармливают огромные объемы случайных, полуслучайных или вообще намеренно испорченных данных, с надеждой выявить уязвимости или баги. Изначально этот метод применялся в академической среде для поиска дыр в безопасности, но быстро перекочевал в руки здравомыслящих разработчиков. Property-based testing, в свою очередь, представляет собой подход к тестированию, где вместо проверки конкретных примеров типа «дважды два — четыре» мы формулируем общие свойства системы. Например: «если функция принимает список и возвращает список, то длина результата не должна превышать длину входа». А дальше уже инструмент генерирует тысячи, миллионы вариантов входных данных и проверяет, соблюдается ли это условие.
Первой ласточкой этого подхода стал QuickCheck, разработанный в конце девяностых Кеном Класессеном и Джоном Хьюзом для языка Haskell. В тот момент, когда бо́льшая часть индустрии с упоением писала unit-тесты вида «если передать три, вернется шесть», эти двое додумались спросить: а почему бы не заставить машину проверить тысячи вариантов сразу? Haskell, с его богатой системой типов и функциональной парадигмой, оказался идеальной почвой. Идея была проста, как топор, и эффективна, как катапульта. За QuickCheck последовали его реинкарнации в других языках: ScalaCheck, Hypothesis для Python, PropEr для Erlang и многие другие.
А теперь пара слов о священной корове индустрии — юнит-тестах. Обещаю, без излишней жестокости, хотя устоять будет сложно.
юнит-тесты покрывают все возможные сценарии — это самое распространенное заблуждение, сравнимое разве что с верой в доброго волшебника, раздающего деньги прямо на улице. Юнит-тест проверяет ровно то, что ему велели проверить. Если вы написали тест на то, что функция умножения правильно перемножает два и три, то получите шесть. А что будет, если передать ноль, отрицательное число, строку, массив, функцию, undefined? Об этом тест промолчит, как рыба на сковородке.
тесты делают код безопасным — ложь в квадрате. Или даже в кубе, если вы склонны к гиперболизации. Тесты лишь проверяют, что код ведет себя предсказуемо для известных входов. Но огромная часть багов появляется на стыке неожиданных данных и редких условий выполнения. Гонки, переполнение буфера, неверные допущения о входных данных — все это проскакивает мимо юнит-тестов, как курьер на самокате мимо пешеходов. Я не говорю о разных ветках в операторах условного ветвления: покрытие 100%, а на деле проверено примерно 10% путей уже для тройной вложенности.
больше тестов — лучше качество кода — по этой логике чем больше краски на холсте, тем лучше картина. Количество тестов не равно качеству покрытия. Тридцать тестов, проверяющих варианты «один плюс один», «два плюс два» и «три плюс три», — это не тридцать шагов к истине, а тридцать способов потратить время впустую. Property-based тесты, напротив, за один запуск проверяют сотни тысяч комбинаций.
написание тестов — это отдельная, изолированная задача — еще одна благостная сказка, рассказанная в корпоративных офисах. На практике юнит-тесты требуют постоянного сопровождения: рефакторинг кода ломает тесты, изменение интерфейса функции заставляет переписывать десятки проверок. Это как держать аквариум с экзотическими рыбками на метеорологической станции Южного полюса: ярко, но очень утомительно.
А теперь о том, почему фаззинг и property-based тестирование должны заменить юнит-тесты. Или, по крайней мере, серьезно потеснить их с пьедестала.
Во-первых, они не знают жалости. Никто не будет деликатно подавать на вход только те значения, которые заботливо прописаны в тестах. В функцию будут швырять все подряд: пустые строки, нули, отрицательные числа, огромные массивы, битые данные, юникод, эмодзи, NULL, строку из миллиона символов. Если код этого не выдерживает — лучше узнать об этом на этапе разработки, а не когда пользователь получит segmentation fault прямо в торец.
Во-вторых, property-based тесты заставляют думать иначе. Вместо того чтобы перечислять примеры, вы формулируете инварианты — то, что должно быть истинно всегда. Это ближе к математике, к настоящему пониманию того, как работает код. Например, если вы пишете функцию сортировки, то property-based тест не станет проверять, что список [3, 1, 2] превращается в [1, 2, 3]. Он проверит, что: ① результат всегда упорядочен, ② длина не изменилась, ③ все элементы исходного списка присутствуют в результате. Ведь именно эти три качества — свойства сортировки. И проверит это на десятках тысяч разных списков.
В-третьих, история. Когда Бартон Миллер в 1988 году ввел термин «фаззинг» на своем семинаре о случайном тестировании Unix-утилит, он мимоходом обнаружил, что от четверти до трети программ падают или зависают при подаче случайных входных данных. Прошло почти сорок лет, а проблема актуальна до сих пор. Между тем QuickCheck и его последователи доказали, что property-based подход находит баги, которые никогда бы не обнаружились традиционными методами. В академических кругах и в таких компаниях, как Ericsson, этот подход уже давно стал стандартом для критически важных систем.
В-четвертых, экономия времени. Как ни парадоксально это звучит, но автоматическая генерация тысяч тестовых случаев отнимает меньше усилий, чем написание и поддержка сотен юнит-тестов вручную. Фаззинг-инструменты могут работать непрерывно, находя все новые и новые способы сломать код. Property-based библиотеки генерируют входные данные на лету, проверяя утверждения, которые вы один раз сформулировали. Ни тем, ни другим инструментам в общем случае даже не требуется доступ к кодовой базе проекта: можно тестировать хоть бинарные модули. А юнит-тесты? Они требуют внимания, заботы и бесконечных правок при каждом чихе в сторону рефакторинга.
Конечно, было бы несправедливо утверждать, что юнит-тесты совершенно бесполезны. Иногда, в очень редких случаях, когда нужно проверить простейшую логику или задокументировать специфический пограничный случай, — они имеют право на существование. Примерно как имеет право на существование конная тяга — где-то в глухой деревне, где не ходят автобусы. Но строить на них стратегию тестирования целого проекта — это признак либо непонимания, либо упорного нежелания двигаться дальше.
Мир тестирования давно вышел за рамки унылого перечисления входов и выходов. Фаззинг нашел тысячи уязвимостей в реальных системах — от браузеров до операционных систем. Property-based тестирование сделало возможным формальную верификацию сложных алгоритмов. А что сделали юнит-тесты? Создали иллюзию качества и отчеты с зелеными галочками для менеджеров.
Так что когда в следующий раз кто-то будет говорить вам о важности стопроцентного покрытия юнит-тестами, вежливо напомните об истории QuickCheck, о работе Бартона Миллера и о том, что настоящая проверка программы — это не когда она работает на примерах, а когда она выживает под напором хаоса.
Комментарии (2)

mvv-rus
08.01.2026 07:05А что сделали юнит-тесты? Создали иллюзию качества и отчеты с зелеными галочками для менеджеров.
Нет, нашли тучу дурацких ошибок - когда что-то забыли (прибавить, например), что-то перепутали (плюс с минусом, к примеру) и так далее. И сейчас модульные тесты заняты тем же самым - они страхуют нас от дурацких ошибок при переписывании кода. А большинство ошибок в реальной коммерческой разработке, а не в rocket science - они именно такие, дурацкие.
А у фаззинга - другое применение: искать ошибки, которые приводят к нарушению выполнения программы. Если дурацкая ошибка не приведет к вылету, фаззинг ее вряд ли найдет.
Во-вторых, property-based тесты заставляют думать иначе. Вместо того чтобы перечислять примеры, вы формулируете инварианты — то, что должно быть истинно всегда
Это работает, когда эти самые инварианты есть и их легко обнаружить. По-моему, это не про разработку коммерческих систем (ну, разве что, для бухучета с помощью двойной записи). - уж больно они громоздкие и имеютмножество вариантов поведения. Ну а уж насчет формальной верификации программ общего назначения - это IMHO вообще мечты. И да, дурацкие ошибки они ловят ничуть не лучше, чем модульные тесты. А в случае изменения фнукиональных требваний эти инварианты с немалой вероятностью придется искать заново - так что и в плане трудемкости сопровождения ничем они не лучше модульных тестов.
Короче, программы это всегда сложно и серебрянной пули, чтобы эту сложность поразить нет.
yappari
Помнится, лет эдак тридцать назад читал книжку авторского дуэта Очкова и Пухначева (128 советов начинающему программисту). Помимо прочего, там было в чём-то схожее предложение - чтобы выйти на вероятный баг, надо в код насовать ещё багов. Насколько это применимо в автоматическом тестировании, сказать затрудняюсь.