Comments 12
Использовать assume в тестах идиоматичнее, чем фильтровать значения с помощью условных выражений.
А, ну, понятно теперь.
#sarcasm
+2
Паша, ты же умный человек, поэтому понимаешь, что это раскрывается как «использование assume в тестах больше соответствует идеологии JUnit, чем фильтрация с помощью...» :)
А вообще, конечно, mea culpa. Не вычистил фразеологизм.
А вообще, конечно, mea culpa. Не вычистил фразеологизм.
0
Ага, понимаю :)
«ну, тут так принято, все пишут именно так, и авторы говорят, что надо так, но если честно, никто не понимает, чем это лучше»
«ну, тут так принято, все пишут именно так, и авторы говорят, что надо так, но если честно, никто не понимает, чем это лучше»
0
Как-раз таки вполне понятно, почему стоит выбирать assume, а не if.
Аргумент номер раз.
Условные выражения в тестах — это общепризнанный антипаттерн. Хороший тест должен быть простым и понятным (это относится в равной степени как к тестам-примерам, так и тестам-теориям). Максимально возможная простота может быть только у линейного кода, а все ветвления и циклы уже вносят излишнюю сложность.
Тем не менее, иногда мы сталкиваемся с ситуациями, в которых нам действительно нужно продолжить выполнение теста для одних ситуаций и продолжить для других (ну вот, хотя бы, если теория верна только для части входных параметров). В таком случае assume поможет нам отфильтровать выражения и в то же время не писать пахнущего кода. Код остаётся простым и линейным:
Аргумент номер два.
Если мы случайно или умышленно написали в assume какую-то бодягу, из-за которой выполнение кода никогда не доходит до реального assert'a, то JUnit отловит эту ситуацию и напишет, что тест проигнорирован (а при использовании теорий вообще свалит тест). Соответственно, ты сможешь увидеть это и принять меры, если требуется. При использовании же условных выражений придётся надеяться лишь на себя самого. Если ты поставил в if аналогичную бодягу, из-за которой дело не доходит до assert-а, то у тебя получится очень вредный и опасный тест: он всегда проходит и вселяет ложную уверенность, что всё хорошо, но при этом на деле ничего никогда не проверяет.
Аргумент номер раз.
if (condition) {
assertEquals(expected, actual);
}
// else do nothing
Условные выражения в тестах — это общепризнанный антипаттерн. Хороший тест должен быть простым и понятным (это относится в равной степени как к тестам-примерам, так и тестам-теориям). Максимально возможная простота может быть только у линейного кода, а все ветвления и циклы уже вносят излишнюю сложность.
Тем не менее, иногда мы сталкиваемся с ситуациями, в которых нам действительно нужно продолжить выполнение теста для одних ситуаций и продолжить для других (ну вот, хотя бы, если теория верна только для части входных параметров). В таком случае assume поможет нам отфильтровать выражения и в то же время не писать пахнущего кода. Код остаётся простым и линейным:
assumeTrue(condition);
assertEquals(expected, actual);
Аргумент номер два.
Если мы случайно или умышленно написали в assume какую-то бодягу, из-за которой выполнение кода никогда не доходит до реального assert'a, то JUnit отловит эту ситуацию и напишет, что тест проигнорирован (а при использовании теорий вообще свалит тест). Соответственно, ты сможешь увидеть это и принять меры, если требуется. При использовании же условных выражений придётся надеяться лишь на себя самого. Если ты поставил в if аналогичную бодягу, из-за которой дело не доходит до assert-а, то у тебя получится очень вредный и опасный тест: он всегда проходит и вселяет ложную уверенность, что всё хорошо, но при этом на деле ничего никогда не проверяет.
+2
Странноватая концепция все-таки.
1. Повторный запуск повалившегося теста не факт, что снова его повалит. Это явно для любителей приключений и людей с закаленными нервами.
2. Получается, что нужно иметь реализацию алгоритма еще до того, как сам алгоритм написан (отлично проиллюстрировано на примере в статье). Ну и какой тогда смысл, спрашивается? Упростить рерайт кода?
1. Повторный запуск повалившегося теста не факт, что снова его повалит. Это явно для любителей приключений и людей с закаленными нервами.
2. Получается, что нужно иметь реализацию алгоритма еще до того, как сам алгоритм написан (отлично проиллюстрировано на примере в статье). Ну и какой тогда смысл, спрашивается? Упростить рерайт кода?
+1
1. Да, это заметный изъян, и я неслучайно обратил на него внимание. Но, возможно, это особенность конкретной библиотеки. Всё-таки, JUnit, насколько мне известно, не сохраняет никаких состояний между запусками, а уж его плагины и подавно.
В презентации, ссылку на которую я ставил в начале статьи, демонстрируется другой инструмент (скорее всего, Quviq QuickCheck), и он-то как раз обладает способностью сохранять падающие примеры между запусками. По крайней мере, создаётся такое впечатление. Более того, он (как и оригинальный QuickCheck из Haskell) использует специальный алгоритм для поиска контрпримера минимального размера. Проекту JUnit-QuickCheck о таком остаётся пока только мечтать. Это печально, потому что в shrink-е явно большая мощь заключена.
2. Такое тестирование вряд ли подойдёт абсолютно для любого алгоритма. Однако не всегда нужна полная реализация алгоритма. В одних случаях мы можем просто сформулировать, какими свойствами должен обладать результат работы алгоритма (вот пример). В других случаях можем построить простую модель тестируемого кода (например, обычный список может служить моделью при тестировании синхронизированного списка) и выводить ожидания через эту модель.
В презентации, ссылку на которую я ставил в начале статьи, демонстрируется другой инструмент (скорее всего, Quviq QuickCheck), и он-то как раз обладает способностью сохранять падающие примеры между запусками. По крайней мере, создаётся такое впечатление. Более того, он (как и оригинальный QuickCheck из Haskell) использует специальный алгоритм для поиска контрпримера минимального размера. Проекту JUnit-QuickCheck о таком остаётся пока только мечтать. Это печально, потому что в shrink-е явно большая мощь заключена.
2. Такое тестирование вряд ли подойдёт абсолютно для любого алгоритма. Однако не всегда нужна полная реализация алгоритма. В одних случаях мы можем просто сформулировать, какими свойствами должен обладать результат работы алгоритма (вот пример). В других случаях можем построить простую модель тестируемого кода (например, обычный список может служить моделью при тестировании синхронизированного списка) и выводить ожидания через эту модель.
+1
ИМХО ни по ссылке, ни в статье примеры не убедительны. Хотелось бы увидить более жизненные случаи, когда описанный в статье способ имеет СУЩЕСТВЕННЫЕ преимущества перед, например, банальным хранением данных в списке вида «вход=>ожидаемый_выход» с последующим перебором их в цикле.
0
К сожалению, не могу привести примеров из собственной практики, потому что я ещё только начал осваивать эту технику. Но, возможно, Вас убедит пример из уже упоминавшейся презентации (можно не смотреть её целиком, если нет времени, а просто скачать слайды и посмотреть на пример, который начинается на 35-й странице).
Автор рассказывает, как он использовал QuickCheck для поиска дефектов в Erlang-библиотеке Dets. Несмотря на то, что библиотека активно использовалась в продакшне в течение многих лет, в ней было два весьма неприятных бага. QuickCheck-тестирование позволило их найти и исправить. Хотя, что интересно, найденные дефекты были в итоге зафиксированы с помощью традиционных тестов-примеров.
Мой личный опыт показывает, что когда мы сталкиваемся с тестированием реальных сложных систем, то на выдуманных из головы примерах в тестировании далеко не уедешь. Вроде бы и проходят все твои тест-кейсы, но в продакшне реальные дефекты начинают возникать после комбинаций действий из разных тестов. Шаг вправо, шаг влево от проверенной дорожки — и привет. Как найти и обезвредить такие дефекты? Писать тесты ещё и на каждую комбинацию действий? На каждые три комбинации действий? На каждые четыре?.. Это же просто нереально :(
Именно после таких ситуаций я и начал смотреть в сторону семейства QuickCheck-утилит. Да, приходится начинать с простых вещей, чтобы понять, как работать со сложными. Но, надеюсь, со временем у меня появятся более наглядные аргументы.
Автор рассказывает, как он использовал QuickCheck для поиска дефектов в Erlang-библиотеке Dets. Несмотря на то, что библиотека активно использовалась в продакшне в течение многих лет, в ней было два весьма неприятных бага. QuickCheck-тестирование позволило их найти и исправить. Хотя, что интересно, найденные дефекты были в итоге зафиксированы с помощью традиционных тестов-примеров.
Мой личный опыт показывает, что когда мы сталкиваемся с тестированием реальных сложных систем, то на выдуманных из головы примерах в тестировании далеко не уедешь. Вроде бы и проходят все твои тест-кейсы, но в продакшне реальные дефекты начинают возникать после комбинаций действий из разных тестов. Шаг вправо, шаг влево от проверенной дорожки — и привет. Как найти и обезвредить такие дефекты? Писать тесты ещё и на каждую комбинацию действий? На каждые три комбинации действий? На каждые четыре?.. Это же просто нереально :(
Именно после таких ситуаций я и начал смотреть в сторону семейства QuickCheck-утилит. Да, приходится начинать с простых вещей, чтобы понять, как работать со сложными. Но, надеюсь, со временем у меня появятся более наглядные аргументы.
0
Мысль вдогонку. Возможно, QuickCheck-подход больше подходит для исследовательского тестирования (exploratory testing), а не для регрессионного. Написали мы спеку для генератора действий, запустили его, подождали пару часиков и нашли несколько багов. Отлично! Зафиксировали эти баги обычными юнит-тестами, а QuickCheck-тесты выкинули. Так мы, с одной стороны, и дефекты правим, и в то же время не тратим кучу времени на запуск огромного количества случайных тестов, подавляющее большинство из которых успешно проходит.
+1
Спасибо за статью! Очень интересно и познавательно.
Сразу после прочтения возникают вопросы о тестировании методов с более сложными объектами, интересно, как это может выглядеть, если вообще возможно. Просмотр презентации, в некотором роде раскрывает области применения, так что часть вопросов снимается.
Дальнейшее гугление показало, что QuickCheck не рассматривается как альтернатива юнит-тестированию, а как дополнение к общему массиву тестов. Например, основные 80% сценариев легче и быстрее возможно покрыть обычными тестами, и только потом уже дописать теории. Так как описать ограничения на свойства тестируемого объекта несколько сложнее. В силу хотя бы того, что непривычно и нет большого пула примеров, что ли.
Сразу после прочтения возникают вопросы о тестировании методов с более сложными объектами, интересно, как это может выглядеть, если вообще возможно. Просмотр презентации, в некотором роде раскрывает области применения, так что часть вопросов снимается.
Дальнейшее гугление показало, что QuickCheck не рассматривается как альтернатива юнит-тестированию, а как дополнение к общему массиву тестов. Например, основные 80% сценариев легче и быстрее возможно покрыть обычными тестами, и только потом уже дописать теории. Так как описать ограничения на свойства тестируемого объекта несколько сложнее. В силу хотя бы того, что непривычно и нет большого пула примеров, что ли.
+3
Надеюсь, моя статья подтолкнёт Вас к самостоятельным экспериментам в данном направлении. Это действительно интересный и полезный опыт!
0
Sign up to leave a comment.
Старая псина учит новые трюки: Code Kata с использованием QuickCheck