Про ScalaCheck. Свойства. Часть 3
В предыдущих частях мы уже успели познакомиться со свойствами и опробовать их в связке с генераторами. В этом туториале мы рассмотрим свойства подробнее. Статья состоит из двух частей: первая — техническая, в ней будет рассказано про комбинаторы свойств, а также другие возможности библиотеки ScalaCheck. Эта часть будет посвящена различным техникам тестирования.
Структура цикла
Комбинаторы свойств
Константные свойства
В Scalacheck существуют постоянные свойства — свойства которые всегда возвращают один и тот же результат. Примерами таких свойств являются:
- Prop.undecided
- Prop.falsified
- Prop.proved
- Prop.passed
- Prop.exception(e: Throwable)
С методами Prop.passed и Prop.falsified мы уже знакомы: Prop.passed соответствует удачному прохождению свойством теста для комбинатора forAll , а Prop.falsified соответствует неудачному прохождению хотя бы одного теста для свойства с комбинатором forAll . В дополнение к ним:
-
Prop.exception возвращается, если внутри вашего свойства
Комбинации свойств
ScalaCheck позволяет вам вкладывать свойства forAll , throws и exists произвольным образом. Продемонстрируем это на примере с forAll :
Prop.throws
Логический метод, возвращающий истину только в случае, если во время выполнения выражения будет выброшено вполне ожидаемое исключение. Вы можете использовать свойства следующим образом:
Однако, особого смысла тестировать константы нет. Проверим Prop.throws при делении на 0 произвольного целого числа:
Prop.forAll
Называемый в логике универсальным квантификатором, является также наиболее часто используемым нами свойством. Условие, переданное в forAll , должно быть или Boolean , или же являться экземпляром класса Prop .
Prop.exists
Ведет себя точь-в-точь как квантор существования. Поведение во многом схоже с forAll , за исключением того, что в случае с данным комбинатором, свойство засчитывается, если хотя бы один элемент множества входных данных удовлетворяет заданному условию. На практике использование Prop.exist является проблематичным в виду того, что может быть достаточно сложно найти случай, удовлетворяющий заданному условию:
При вызове p1.check ScalaCheck выведет нам следующее:
А теперь попробуем попросить у ScalaCheck невозможного:
Как только ScalaCheck найдет первый устраивающий нас элемент, он сообщит о том, что свойство доказано (proved), а не протестированно (passed).
Наличие Prop.exists определенно решает чьи-то проблемы. В моей практике это свойство использовать не приходилось.
Именование свойств
Именование является хорошей практикой как для генераторов, так и для свойств. При именовании свойств используются те же операторы, что и для генераторов: в качестве имени свойства может использоваться строка либо символ. Используются операторы :| и |: .
Логические операторы
Свойства представляют собой логические выражения. В ScalaCheck вы можете использовать логические операторы применительно к свойствам. Внутри Prop объявлены операторы && и || , поведение которых в точности совпадает с одноименными операторами класса Boolean . В дополнение к названным выше операторам, существуют синонимы с символьными именами: Prop.all и Prop.atLeastOne .
Использование логических операторов позволяет собирать сложные свойства из более простых. Более того, вы также можете объединять экземпляры Prop и переменные логического типа в одном выражении: для этого вам необходимо явным образом добавить в область видимости Prop.propBoolean , так как это один из тех случаев, когда компилятор Scala не может автоматически выполнить приведение типов. Если же вы хотите выполнить преобразование явно, вы можете поступить следующим образом:
Итак, рассмотрим пример, для списка и метода reversed :
Этот метод замечательно описывает основное свойство метода reversed и его вполне достаточно. Однако, нашей задачей сейчас является не четкая формулировка свойства, а демонстрация возможностей ScalaCheck. Поэтому притянем за уши еще пару свойств, которые неявным образом выражены в elementsAreReversed :
Эти свойства являются булевыми значениями. Добавление метки (при наличии propBoolean в области видимости) автоматически сконвертирует наши переменные к типу Prop . Теперь давайте опишем наше первое составное свойство и заодно воспользуемся метками:
Когда получили не то что хотели
Хотели бы вы в случае ошибки видеть какое из значений мы имеем, а какое ожидали? ScalaCheck дает вам такую возможность: всего-лишь следует заменить тривиальное равенство == на операторы ?= или =? . Как только вы это сделаете, ScalaCheck запомнит обе части выражения при выполнении данного свойства, и в случае, если свойство окажется неверным, вам будут представлены оба значения:
Для того чтобы воспользоваться операторами ?= и =? , вам необходимо добавить Prop.AnyOperators в область видимости:
Актуальным, будет значение располагающееся ближе к знаку ? , Ожидаемым, будет значение, ближайшее к знаку равенства.
Вы также можете проинтегрироваться cо ScalaTest и использовать прилагающиеся к нему матчеры для того чтобы получать читаемые сообщения об ошибках. Подробнее об этом будет рассказано в разделе «Интеграция и настройки».
Собираем статистику
classify
Даже если все ваши тесты выполняются успешно и все хорошо, возможно, вы захотите получить информацию, которая была использована при тестировании. Например, если у вас есть нетривиальные предусловия для метода, и вы точно хотите знать насколько жестко ScalaCheck выбирает входные данные. Так что если вам нужна статистика, Prop.classify к вашим услугам:
Вы можете добавить столько классификаторов, сколько сочтете нужным, ScalaCheck сольет их воедино и представит в виде распределения:
collect
В дополнение к classify существует более обобщенный метод для сбора и статистики: метод Prop.collect собирает любую интересную вам статистику и группирует ее под наиболее удобным для вас именем:
Кстати, имя может быть любого типа: toString будет вызван автоматически. Рассмотрим простейший пример:
После вызова метода check имеем:
Эталонная реализация
Итак, представьте, вы пишете очередную реализацию списка. Ваша реализация определенно лучше других (по-крайней мере, автор на это рассчитывает). И вот пришла пора ваш список тестировать.
Было бы очень здорово каким-либо образом протестировать ваш список используя, например, уже реализованный в JDK класс ConcurrentHashMap . И вы можете это сделать: вместо того чтобы создавать спецификацию, содержащую набор жестких условий и контрактов, вы можете задать спецификацию неявно, используя уже известную работающую реализацию (эталонную). Данный подход широко используется в тестировании. В англоязычных источниках, вы можете найти его под названием reference implementation.
Симметричные свойства
Также именуемые round-trip properties. Проще не придумать: мы берем некую обратимую функцию и применяем ее два раза, тем самым тестируя ее на обратимость:
Данное свойство не описывает метод neg полностью, как и не говорит о его функциональности. Однако, оно говорит о его обратимости.
Возможно, данный пример, как и пресловутый List.reverse , который вы можете найти ни в одном десятке туториалов, покажется вам примитивным. Однако, существуют более сложные системы, для которых данный подход применим: парсеры и кодировщики всех сортов и мастей. Например, при использовании симметричных свойств для тестирования парсеров, вы можете найти ошибки в весьма труднодоступных местах.
Метод, представленный ниже, разбирает текст и создает на его основе абстрактное синтаксическое дерево (AST), а метод prettyPrint конвертирует это дерево обратно в текст:
В заключение
В большинстве туториалов по ScalaCheck вас сразу же познакомят со свойствами и их надуманной классификацией, а потом будут рассказывать о сложности вычленения свойств из уже написанного кода. Отчасти это верно: без должной тренировки достаточно непросто выделить свойства, которые можно протестировать.
Однако, по своему скромному опыту использования ScalaCheck, самым сложным, все-таки является составление генераторов. Этот процесс требует больше усилий, нежели написание свойств: для написания одного свойства может потребоваться написание десятка-другого генераторов. Именно поэтому я начал рассказ с генераторов, что многим, возможно могло показаться странным.