Как стать автором
Обновить

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

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

А, ну, понятно теперь.
#sarcasm
Паша, ты же умный человек, поэтому понимаешь, что это раскрывается как «использование assume в тестах больше соответствует идеологии JUnit, чем фильтрация с помощью...» :)

А вообще, конечно, mea culpa. Не вычистил фразеологизм.
Ага, понимаю :)
«ну, тут так принято, все пишут именно так, и авторы говорят, что надо так, но если честно, никто не понимает, чем это лучше»
Как-раз таки вполне понятно, почему стоит выбирать assume, а не if.

Аргумент номер раз.

if (condition) {
  assertEquals(expected, actual);
}
// else do nothing

Условные выражения в тестах — это общепризнанный антипаттерн. Хороший тест должен быть простым и понятным (это относится в равной степени как к тестам-примерам, так и тестам-теориям). Максимально возможная простота может быть только у линейного кода, а все ветвления и циклы уже вносят излишнюю сложность.

Тем не менее, иногда мы сталкиваемся с ситуациями, в которых нам действительно нужно продолжить выполнение теста для одних ситуаций и продолжить для других (ну вот, хотя бы, если теория верна только для части входных параметров). В таком случае assume поможет нам отфильтровать выражения и в то же время не писать пахнущего кода. Код остаётся простым и линейным:

assumeTrue(condition);
assertEquals(expected, actual);

Аргумент номер два.

Если мы случайно или умышленно написали в assume какую-то бодягу, из-за которой выполнение кода никогда не доходит до реального assert'a, то JUnit отловит эту ситуацию и напишет, что тест проигнорирован (а при использовании теорий вообще свалит тест). Соответственно, ты сможешь увидеть это и принять меры, если требуется. При использовании же условных выражений придётся надеяться лишь на себя самого. Если ты поставил в if аналогичную бодягу, из-за которой дело не доходит до assert-а, то у тебя получится очень вредный и опасный тест: он всегда проходит и вселяет ложную уверенность, что всё хорошо, но при этом на деле ничего никогда не проверяет.
Странноватая концепция все-таки.
1. Повторный запуск повалившегося теста не факт, что снова его повалит. Это явно для любителей приключений и людей с закаленными нервами.
2. Получается, что нужно иметь реализацию алгоритма еще до того, как сам алгоритм написан (отлично проиллюстрировано на примере в статье). Ну и какой тогда смысл, спрашивается? Упростить рерайт кода?
1. Да, это заметный изъян, и я неслучайно обратил на него внимание. Но, возможно, это особенность конкретной библиотеки. Всё-таки, JUnit, насколько мне известно, не сохраняет никаких состояний между запусками, а уж его плагины и подавно.

В презентации, ссылку на которую я ставил в начале статьи, демонстрируется другой инструмент (скорее всего, Quviq QuickCheck), и он-то как раз обладает способностью сохранять падающие примеры между запусками. По крайней мере, создаётся такое впечатление. Более того, он (как и оригинальный QuickCheck из Haskell) использует специальный алгоритм для поиска контрпримера минимального размера. Проекту JUnit-QuickCheck о таком остаётся пока только мечтать. Это печально, потому что в shrink-е явно большая мощь заключена.

2. Такое тестирование вряд ли подойдёт абсолютно для любого алгоритма. Однако не всегда нужна полная реализация алгоритма. В одних случаях мы можем просто сформулировать, какими свойствами должен обладать результат работы алгоритма (вот пример). В других случаях можем построить простую модель тестируемого кода (например, обычный список может служить моделью при тестировании синхронизированного списка) и выводить ожидания через эту модель.
ИМХО ни по ссылке, ни в статье примеры не убедительны. Хотелось бы увидить более жизненные случаи, когда описанный в статье способ имеет СУЩЕСТВЕННЫЕ преимущества перед, например, банальным хранением данных в списке вида «вход=>ожидаемый_выход» с последующим перебором их в цикле.
К сожалению, не могу привести примеров из собственной практики, потому что я ещё только начал осваивать эту технику. Но, возможно, Вас убедит пример из уже упоминавшейся презентации (можно не смотреть её целиком, если нет времени, а просто скачать слайды и посмотреть на пример, который начинается на 35-й странице).

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

Мой личный опыт показывает, что когда мы сталкиваемся с тестированием реальных сложных систем, то на выдуманных из головы примерах в тестировании далеко не уедешь. Вроде бы и проходят все твои тест-кейсы, но в продакшне реальные дефекты начинают возникать после комбинаций действий из разных тестов. Шаг вправо, шаг влево от проверенной дорожки — и привет. Как найти и обезвредить такие дефекты? Писать тесты ещё и на каждую комбинацию действий? На каждые три комбинации действий? На каждые четыре?.. Это же просто нереально :(

Именно после таких ситуаций я и начал смотреть в сторону семейства QuickCheck-утилит. Да, приходится начинать с простых вещей, чтобы понять, как работать со сложными. Но, надеюсь, со временем у меня появятся более наглядные аргументы.
Мысль вдогонку. Возможно, QuickCheck-подход больше подходит для исследовательского тестирования (exploratory testing), а не для регрессионного. Написали мы спеку для генератора действий, запустили его, подождали пару часиков и нашли несколько багов. Отлично! Зафиксировали эти баги обычными юнит-тестами, а QuickCheck-тесты выкинули. Так мы, с одной стороны, и дефекты правим, и в то же время не тратим кучу времени на запуск огромного количества случайных тестов, подавляющее большинство из которых успешно проходит.
Спасибо за статью! Очень интересно и познавательно.

Сразу после прочтения возникают вопросы о тестировании методов с более сложными объектами, интересно, как это может выглядеть, если вообще возможно. Просмотр презентации, в некотором роде раскрывает области применения, так что часть вопросов снимается.

Дальнейшее гугление показало, что QuickCheck не рассматривается как альтернатива юнит-тестированию, а как дополнение к общему массиву тестов. Например, основные 80% сценариев легче и быстрее возможно покрыть обычными тестами, и только потом уже дописать теории. Так как описать ограничения на свойства тестируемого объекта несколько сложнее. В силу хотя бы того, что непривычно и нет большого пула примеров, что ли.
Надеюсь, моя статья подтолкнёт Вас к самостоятельным экспериментам в данном направлении. Это действительно интересный и полезный опыт!
Уже подтолкнула к тому, чтобы покопать интернет на счет таких проверок. Для .NET нашел наиболее свежий\живой проект портирования QuickCheck. Так что буду исследовать тоже, смотреть, что можно с этим делать в условиях близких к продакшену.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории