Обновить

Комментарии 12

или вообще намеренно испорченных данных, с надеждой выявить уязвимости или баги.

Помнится, лет эдак тридцать назад читал книжку авторского дуэта Очкова и Пухначева (128 советов начинающему программисту). Помимо прочего, там было в чём-то схожее предложение - чтобы выйти на вероятный баг, надо в код насовать ещё багов. Насколько это применимо в автоматическом тестировании, сказать затрудняюсь.

Применимо. Называется «мутационное тестирование». Я даже сделал несколько попыток принести его в эликсир, но с компилируемыми языками есть плохо решаемая проблема производительности: на каждый чих (мутацию) надо перекомпилировать проект, а это очень долго и дорого.

А что сделали юнит-тесты? Создали иллюзию качества и отчеты с зелеными галочками для менеджеров.

Нет, нашли тучу дурацких ошибок - когда что-то забыли (прибавить, например), что-то перепутали (плюс с минусом, к примеру) и так далее. И сейчас модульные тесты заняты тем же самым - они страхуют нас от дурацких ошибок при переписывании кода. А большинство ошибок в реальной коммерческой разработке, а не в rocket science - они именно такие, дурацкие.

А у фаззинга - другое применение: искать ошибки, которые приводят к нарушению выполнения программы. Если дурацкая ошибка не приведет к вылету, фаззинг ее вряд ли найдет.

Во-вторых, property-based тесты заставляют думать иначе. Вместо того чтобы перечислять примеры, вы формулируете инварианты — то, что должно быть истинно всегда

Это работает, когда эти самые инварианты есть и их легко обнаружить. По-моему, это не про разработку коммерческих систем (ну, разве что, для бухучета с помощью двойной записи). - уж больно они громоздкие и имеютмножество вариантов поведения. Ну а уж насчет формальной верификации программ общего назначения - это IMHO вообще мечты. И да, дурацкие ошибки они ловят ничуть не лучше, чем модульные тесты. А в случае изменения фнукиональных требваний эти инварианты с немалой вероятностью придется искать заново - так что и в плане трудемкости сопровождения ничем они не лучше модульных тестов.

Короче, программы это всегда сложно и серебрянной пули, чтобы эту сложность поразить нет.

Ну а уж насчет формальной верификации программ общего назначения — это IMHO вообще мечты. И да, дурацкие ошибки они ловят ничуть не лучше, чем модульные тесты.

Формальная верификация (строгое доказательство) — ничуть не лучше, чем случайный перебор 3% вариантов (см. комментарий про комбинаторику ниже)? Ясно.

И пока для вас это мечты, я лет восемь уже критические части наших бизнес-подсистем верифицирую формально, и даже язык для этого написал, который компилируется в тот же байт-код, что и остальная кодовая база.

Вы попутали. Это было написано не про формальную верификацию, а про property-based тесты. А что до формальной верификации, то в общем случае она невозможна (теорема Тьюринга), а всякие частные случаи - трудоемки, в массовой разработке на земле это применять слишком дорого, прежде всего - с точки зрения квалификации персонала.

То есть вы там можете сделать свой аналоговнет, но в массы это не пойдет - массам нужно что попроще. А массам бизнесов - то, что можно поручить Искусственному Идиоту.

PS Как я понимаю, у вас не нашлось возражений против моей точки зрения почему модульные тесты все же нужны, вопреки тому, что вы тут понаписали в статье.

Попутал, конечно. Я же поленился дать полную цитату.

Ой, постойте. Не поленился.

Мальчик, иди в песочницу, а?

Таки поленился - там спереди ещё фраза была.

А по сути - я понимаю, что вам сложно отказываться от мысли, что количество знаний ценно само по себе. Но это именно так - излишние знания не оплачиваются, если есть возможность без них обойтись, а экономике выгодно, когда обойтись без них можно - это снижает издержки общественного производства.

PS Хамить-то зачем?
PPS Так я правильно понял, что против модульных тестов вы больше ничего не имеете?

Я пейсал в своё время по тестам наброс — https://habr.com/ru/articles/560370/

Там есть что добавить, но в целом всё более-менее актуально.

Добавить — то, что в дискуссии про «100% покрытие кода тестами не учитывает комбинаторику разных путей исполнения» я не знал, что основная часть фазового пространства находится в библиотеках.

То есть, в примере «Прощание с иллюзиями или 33 банальности» я не вызываю никаких сторонних библиотек, а стоило бы. Причём стоило бы вызвать что-то с огромным кол-вом ветвлений. Например, парсер. Ну regex какой-нибудь.

Да, про комбинаторику — это прям в точку, я забыл по собственному идиотизму. Фразу из текста выше: «тексты — это код» — надо буквально из бронзы отливать и вешать над воротами входа в ойти.

Заметьте, тесты свойств — это тоже код. Но это код, написанный на языке в другой парадигме. Все эти QuickCheck и т.д. — это хитрые движки Монте-Карло, имеющие в качестве DSL что-то вроде программирования в ограничениях — MiniZinc/Picat/Prolog+CLP(FD). Как правило, это резко отличается от парадигмы, в которой написан тестируемый код.

То есть, программисту приходится взглянуть на задачу под другим углом. А это частично решает проблему 2 + 2 = 5.

Мне лет шесть назад довелось послушать Джона Хьюза (создателя QuickCheck) на λ-days, а потом мы оказались с ним за одним столом на ланче, а потом — и вечером в пабе. (Чувак волшебный, он прям из того же гранита отлит, что и Кай, Армстронг, Вирдинг все вот эти вот ребята.)

И он мне рассказал, что QC был создан для автоматизации крупного проекта по тестированию гигантского куска закрытого кода. То есть, у них натурально был черный ящик; доступа к исходникам — не было.

программисту приходится взглянуть на задачу под другим углом

Ну да. Тестировать коммутативность сложения, а не проверять юниттестом, что 2+3=5 и 3+2=5.

Я собираюсь рассказать о том, как правильно тестировать код в изоляции (интеграционные тесты — зверь из соседнего вольера, и о нем — в другой раз).

Вы собирались, рассказать, но не рассказали. Описанные вами способы тестирования полезны, но они, на мой взгляд, не закрывают потребности в тестировании, в которых хороши модульные тесты.

Ключевые особенности модульных тестов:

  1. Изолированность: Тест проверяет только одну конкретную логику. Внешние зависимости заменяются на заглушки (mocks/stubs).

  2. Детерминированность: Результат теста всегда одинаков при одних и тех же входных данных. Нет места случайности.

  3. Скорость: Модульные тесты выполняются очень быстро (миллисекунды), что позволяет запускать их тысячи раз в день.

  4. Автоматизация: Они пишутся как код и запускаются автоматически (часто в CI/CD).

  5. Фокус на поведение: Тест проверяет, что код делает то, что задумано, а не как он это делает (тестируется поведение, а не реализация).

А теперь сравним:

  1. Наверно для ваших тестов изолированность желательна, но не обязательна, особенно если вы упоминаете чёрный ящик и тестирование бинарников, что бы это не значило))

  2. Ваши тесты не детерменированны. Если вы говорите про случайные данные, про автоматическую генерацию, то нет и детерменированности. Т.е. они не гарантирую проверку определенных сценариев, будут очень плохие в CI/CD (см пункт 4).

  3. Если будет много тестов, то и их время выполнения большое. Например, вы проверяете функцию умножения и у вас 2 параметр. Не имеет смысла проверять 100500 выриантов этих параметров. При написании модульных тестов вам нужно проверить 1 базовый сценарий и несколько "корнер кейсов" , например отрицательные числа, переполнение, точность, нули, числа близкие к 0 и т.п. Ваши тесты убьют кучу ресурсов на проверку простой логики, зачем? Но в реальных сценариях параметров много, даже если предположить, что тесты у вас изолированеые. В модульных тестах в идеале вы тестируте поведение - все возможные разумные ветвления логики, в том числе те, в которых код едва ли окажется. Т.е. каждое ветвление логики умножает на 2 количество вариантов работы кода. Если ветвлений у вас 10, то и вариантов у вас будет 2^10. Модульные тесты в изоляции в идеале позволяют вам зафиксировать 9 из 10 ветвлений (часть параметрами с учётом проверок в ветвлениях, часть в моках зависимостей), и вам остаётся написать только 2 теста для проверки одного ветвления логики. Т.е. в этом случае у вас будет 20 тестов на базовые сценарии. Предложенные вами подходы потратят много ресурсов и Не гарантрируют результаты проверки. А что если ветвлений логики будет 30, 40? Кстати, именно поэтому методы должны быть короткими))) это флэшбэк на другую вашу статью, иначе тестировать их буде сложно.

  4. Тесты, предложеные вами - это не про CI/CD, надеюсь это очевидно (мигающий тесты и красные сборки)

  5. Фокус на поведение - эта ОЧЕНЬ важно. Например поведение может быть некорректное, но оно важно именно такое в силу каких-то обстоятельств. А ещё модульный тест это по сути документация, того как должен работать код. Модульные тесты используются для воспроизведения проблем и контроля регрессии. Для быстрой реализации минимума необходимого функционала. Для отладки кода. Безопасность рефакторинга в аспектах поведения. Ваши тесты в этом совсем не помогут ((

Предложеные вами подходы, наверное, можно использовать что бы понять какие "корнер кейсы" забыли проверить модульным тестами, но результат не гарантирован, а следовательно полагаться на них нельзя. Резюме - они полезны, но абсолютно точно вы ошибаетесь в этом:

А теперь о том, почему фаззинг и property-based тестирование должны заменить юнит-тесты. Или, по крайней мере, серьезно потеснить их с пьедестала.

PS. С проверкой забытых "корнер кейсов" в модульных тестах сейчас прекрасно справляется LLM.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации