Часть 3. Свойства В предыдущих частях мы уже успели познакомиться со свойствами и опробовать их в связке с генераторами. В этом туториале мы рассмотрим свойства подробнее. Статья состоит из двух частей: первая — техническая, в ней будет рассказано про комбинаторы свойств, а также другие возможности библиотеки ScalaCheck. Эта часть будет посвящена различным техникам тестирования. Структура цикла В Scalacheck существуют постоянные свойства — свойства которые всегда возвращают один и тот же результат. Примерами таких свойств являются: С методами Prop.passed и Prop.falsified мы уже знакомы: Prop.passed соответствует удачному прохождению свойством теста для комбинатора forAll, а Prop.falsified соответствует неудачному прохождению хотя бы одного теста для свойства с комбинатором forAll. В дополнение к ним: ScalaCheck позволяет вам вкладывать свойства forAll, throws и exists произвольным образом. Продемонстрируем это на примере с forAll: Логический метод, возвращающий истину только в случае, если во время выполнения выражения будет выброшено вполне ожидаемое исключение. Вы можете использовать свойства следующим образом: Однако, особого смысла тестировать константы нет. Проверим Prop.throws при делении на 0 произвольного целого числа: Называемый в логике универсальным квантификатором, является также наиболее часто используемым нами свойством. Условие, переданное в forAll, должно быть или Boolean, или же являться экземпляром класса Prop. Ведет себя точь-в-точь как квантор существования. Поведение во многом схоже с 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 и использовать прилагающиеся к нему матчеры для того чтобы получать читаемые сообщения об ошибках. Подробнее об этом будет рассказано в разделе «Интеграция и настройки». Даже если все ваши тесты выполняются успешно и все хорошо, возможно, вы захотите получить информацию, которая была использована при тестировании. Например, если у вас есть нетривиальные предусловия для метода, и вы точно хотите знать насколько жестко ScalaCheck выбирает входные данные. Так что если вам нужна статистика, Prop.classify к вашим услугам: Вы можете добавить столько классификаторов, сколько сочтете нужным, ScalaCheck сольет их воедино и представит в виде распределения: В дополнение к classify существует более обобщенный метод для сбора и статистики: метод Prop.collect собирает любую интересную вам статистику и группирует ее под наиболее удобным для вас именем: Кстати, имя может быть любого типа: toString будет вызван автоматически. Рассмотрим простейший пример: После вызова метода check имеем: Итак, представьте, вы пишете очередную реализацию списка. Ваша реализация определенно лучше других (по-крайней мере, автор на это рассчитывает). И вот пришла пора ваш список тестировать. Было бы очень здорово каким-либо образом протестировать ваш список используя, например, уже реализованный в JDK класс ConcurrentHashMap. И вы можете это сделать: вместо того чтобы создавать спецификацию, содержащую набор жестких условий и контрактов, вы можете задать спецификацию неявно, используя уже известную работающую реализацию (эталонную). Данный подход широко используется в тестировании. В англоязычных источниках, вы можете найти его под названием reference implementation. Также именуемые round-trip properties. Проще не придумать: мы берем некую обратимую функцию и применяем ее два раза, тем самым тестируя ее на обратимость: Данное свойство не описывает метод neg полностью, как и не говорит о его функциональности. Однако, оно говорит о его обратимости. Возможно, данный пример, как и пресловутый List.reverse, который вы можете найти ни в одном десятке туториалов, покажется вам примитивным. Однако, существуют более сложные системы, для которых данный подход применим: парсеры и кодировщики всех сортов и мастей. Например, при использовании симметричных свойств для тестирования парсеров, вы можете найти ошибки в весьма труднодоступных местах. Метод, представленный ниже, разбирает текст и создает на его основе абстрактное синтаксическое дерево (AST), а метод prettyPrint конвертирует это дерево обратно в текст: В большинстве туториалов по ScalaCheck вас сразу же познакомят со свойствами и их надуманной классификацией, а потом будут рассказывать о сложности вычленения свойств из уже написанного кода. Отчасти это верно: без должной тренировки достаточно непросто выделить свойства, которые можно протестировать. Однако, по своему скромному опыту использования ScalaCheck, самым сложным, все-таки является составление генераторов. Этот процесс требует больше усилий, нежели написание свойств: для написания одного свойства может потребоваться написание десятка-другого генераторов. Именно поэтому я начал рассказ с генераторов, что многим, возможно могло показаться странным. Lead software engineer