Статьи

Тестирование недвижимости с помощью Vavr

Обычное модульное тестирование основано на примерах: мы определяем вход для нашего модуля и проверяем ожидаемый результат. Таким образом, мы можем гарантировать, что для конкретного примера наш код работает правильно. Однако это не доказывает правильность для всех возможных входных данных. Тестирование на основе свойств, также называемое проверкой свойств, использует другой подход, придерживаясь научной теории фальсифицируемости : до тех пор, пока нет доказательств того, что предлагаемое свойство нашего кода является ложным, оно считается истинным. В этой статье мы используем Vavr ( ранее называемый Javaslang ) для тестирования на основе свойств для проверки простой реализации FizzBuzz.

Прежде чем мы начнем

Я использую Vavr Stream s для генерации бесконечной последовательности FizzBuzz, которая должна удовлетворять определенным свойствам. По сравнению с Java 8 Stream s основное преимущество в этой статье заключается в том, что вариант Vavr предоставляет хороший API для извлечения отдельных элементов с помощью функции get .

Тестирование на основе свойств

Тестирование на основе свойств (PBT) позволяет разработчикам определять высокоуровневое поведение программы в терминах инвариантов, которые она должна заполнять. Платформа PBT создает контрольные примеры, чтобы убедиться, что программа работает так, как указано.

Свойство — это комбинация инварианта с генератором входных значений. Для каждого сгенерированного значения инвариант обрабатывается как предикат и проверяется, дает ли он значение true или false для этого значения.

Как только есть одно значение, которое возвращает false, свойство считается фальсифицированным, и проверка отменяется. Если свойство не может быть сфальсифицировано после определенного количества выборочных данных, свойство считается выполненным .

Пример обратного списка

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

Предположим, что генератор генерирует следующие входные значения: [1,2,3] , ["a","b"] и [4,5,6] . Функция обратного списка возвращает [3,2,1] для [1,2,3] . Первый элемент списка — это последний измененный элемент того же списка. Проверка дает истину. Функция обратного списка возвращает ["a", "b"] для ["a", "b"] . Первый элемент списка не является последним измененным элементом того же списка. Проверка дает ложь. Собственность сфальсифицирована.

В Vavr количество попыток фальсифицировать свойство по умолчанию составляет 1000.

Проверка недвижимости FizzBuzz

FizzBuzz — это детская игра, рассчитанная на 1 человека. Каждый ребенок в свою очередь должен произнести текущее число вслух. Если текущее число делится на 3, ребенок должен сказать «Fizz», если оно делится на 5, ответ должен быть «Buzz», а если он делится на 3 и 5, ответ должен быть «FizzBuzz». Итак, игра начинается так:

1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz,…

FizzBuzz часто используется в качестве примера упражнения на собеседовании, где кандидаты должны fizzbuzz цифры от 1 до 100. FizzBuzz может также использоваться в качестве достаточно простого Kata, чтобы сосредоточиться на среде, такой как язык, библиотеки или инструменты, а не на бизнес-логике.

Поток, кажется, хороший способ моделировать FizzBuzz. Мы начнем с пустого и постепенно улучшим его, чтобы пройти наши тесты.

 public Stream<String> fizzBuzz() { Stream.empty(); } 

Каждый третий элемент должен начинаться с Fizz

Из описания FizzBuzz мы можем получить первое свойство Stream FizzBuzz: каждый третий элемент должен начинаться с Fizz . Внедрение Vavr для проверки свойств предоставляет гибкие API для создания генераторов и свойств.

 @Test public void every_third_element_starts_with_Fizz) { Arbitrary<Integer> multiplesOf3 = Arbitrary.integer() .filter(i -> i > 0) .filter(i -> i % 3 == 0); CheckedFunction1<Integer, Boolean> mustStartWithFizz = i -> fizzBuzz().get(i - 1).startsWith("Fizz"); CheckResult result = Property .def("Every third element must start with Fizz") .forAll(multiplesOf3) .suchThat(mustStartWithFizz) .check(); result.assertIsSatisfied(); } 

Давайте пройдемся по одному:

  • Arbitrary.integer() генерирует произвольные целые числа, которые фильтруются для положительных кратных 3, используя filter .
  • Функция mustStartWithFizz является инвариантом: для данного индекса соответствующий элемент в потоке ( get 0-indexed) должен начинаться с Fizz . Для вызова suchThat CheckedFunction1 , которые приведены ниже, требуется CheckedFunction1 , которая похожа на функцию, но может CheckedFunction1 проверенное исключение (то, что нам здесь не нужно).
  • Property::def берет краткое описание и создает Property . Вызов forAll принимает Arbitrary и гарантирует, что инвариант в suchThat должен сохраняться для всех входных значений, генерируемых Arbitrary .
  • Функция check использует генератор для создания кратных 3 в диапазоне от 0 до 100. Она возвращает CheckResult который может быть запрошен относительно результата или утвержден.

Свойство не выполняется, поскольку для каждого сгенерированного кратного 3 соответствующий элемент не существует в пустом Stream . Давайте исправим это, предоставив подходящий поток.

 public Stream<String> fizzBuzz() { return Stream.from(1).map(i -> i % 3 == 0 ? "Fizz": ""); } 

Stream::from создает бесконечный поток, который считается от заданного числа до. Каждый кратный 3 сопоставляется с Fizz . Результирующий поток содержит Fizz для каждого третьего элемента. Как только мы снова проверим свойство, оно удовлетворяется, и результат выводится на консоль:

Каждый третий элемент должен начинаться с Fizz: ОК, пройдено 1000 тестов за 111 мс.

Тестирование недвижимости с помощью Vavr

Увеличить размер выборки

По умолчанию check пытается фальсифицировать свойство 1000 раз. Каждый раз данный Arbitrary генерирует выборочное значение, которое зависит от параметра size . По умолчанию для параметра размера установлено значение 100. Для Arbitrary.integer() параметр отрицательного размера определяет нижнюю границу, а параметр положительного размера определяет верхнюю границу для сгенерированного значения. Это означает, что сгенерированное целое число имеет значение от -100 до 100. Мы также ограничиваем сгенерированные значения только положительными и кратными 3. Это означает, что мы проверяем каждое кратное 3 в диапазоне от 1 до 100 приблизительно 30 раз.

К счастью, Vavr позволяет нам адаптировать размер и количество попыток, предоставляя соответствующие параметры для функции проверки.

 CheckResult result = Property .def("Every third element must start with Fizz") .forAll(multiplesOf3) .suchThat(mustStartWithFizz) .check(10_000, 1_000); 

Теперь сгенерированные кратные 3 взяты из диапазона от 1 до 10 000, и свойство проверяется 1000 раз.

Каждый кратный из 5 должен заканчиваться Buzz

Второе свойство, которое может быть получено из описания FizzBuzz, заключается в том, что каждое кратное 5 должно заканчиваться Buzz .

 @Test public void every_fifth_element_ends_with_Buzz() { Arbitrary<Integer> multiplesOf5 = Arbitrary.integer() .filter(i -> i > 0) .filter(i -> i % 5 == 0); CheckedFunction1<Integer, Boolean> mustEndWithBuzz = i -> fizzBuzz().get(i - 1).endsWith("Buzz"); Property.def("Every fifth element must end with Buzz") .forAll(multiplesOf5) .suchThat(mustEndWithBuzz) .check(10_000, 1_000) .assertIsSatisfied(); } 

На этот раз проверка свойства и утверждение совмещены.

Это свойство фальсифицируется так же, как изначально проверял Buzz . Но на этот раз не потому, что нет элементов (с момента последнего теста функция fizzBuzz создает бесконечный поток), а потому, что каждый пятый элемент является пустой строкой или иногда Fizz, но никогда не Buzz .

Давайте исправим это, возвращая Buzz для каждого пятого элемента.

 public Stream<String> fizzBuzz() { return Stream.from(1).map(i -> i % 3 == 0 ? "Fizz" : (i % 5 == 0 ? "Buzz" : "") ); } 

Когда мы снова проверяем свойство, оно подделывается после некоторых пройденных тестов:

Каждый пятый элемент должен заканчиваться Buzz: Falsified после 7 пройденных тестов за 56 мс.

Сообщение в AssertionError описывает пример, который фальсифицировал свойство:

Ожидаемый удовлетворенный результат проверки, но был фальсифицирован (propertyName = Каждый пятый элемент должен заканчиваться Buzz, count = 8, sample = (30)).

Причина в том, что 30 делится на 3 и на 5, но в реализации fizzBuzz() выше делимость на 3 имеет более высокий приоритет, чем делимость на 5. Однако изменение приоритета не решает проблему, потому что первое свойство, которое каждый третий элемент должен начинаться с Fizz , будет фальсифицирован. Решение — вернуть FizzBuzz, если он делится на 3 и 5.

 private Stream<String> fizzBuzz() { return Stream.from(1).map(i -> { boolean divBy3 = i % 3 == 0; boolean divBy5 = i % 5 == 0; return divBy3 && divBy5 ? "FizzBuzz" : divBy3 ? "Fizz" : divBy5 ? "Buzz" : ""; }); } 

Оба свойства удовлетворены этим подходом.

Каждый не третий и не пятый элемент должен оставаться числом

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

 @Test public void every_non_fifth_and_non_third_element_is_a_number() { Arbitrary<Integer> nonFifthNonThird = Arbitrary.integer() .filter(i -> i > 0) .filter(i -> i % 5 != 0) .filter(i -> i % 3 != 0); CheckedFunction1<Integer, Boolean> mustBeANumber = i -> fizzBuzz().get(i - 1).equals(i.toString()); Property.def("Non-fifth and non-third element must be a number") .forAll(nonFifthNonThird) .suchThat(mustBeANumber) .check(10_000, 1_000) .assertIsSatisfied(); } 

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

 private Stream<String> fizzBuzz() { return Stream.from(1).map(i -> { boolean divBy3 = i % 3 == 0; boolean divBy5 = i % 5 == 0; return divBy3 && divBy5 ? "FizzBuzz" : divBy3 ? "Fizz" : divBy5 ? "Buzz" : i.toString(); }); } 

Другие свойства

Проверка этих трех свойств достаточна для формирования функции fizzBuzz . Нет необходимости в специальном свойстве FizzBuzz, поскольку этот случай уже охватывается свойствами Fizz и Buzz . Однако мы могли бы добавить другое свойство, которое не должно содержаться для всех элементов. Например, в FizzBuzz не должно быть пробелов между Fizz и Buzz . Я думаю, что это хорошее упражнение, и я оставляю это вам, мой дорогой читатель, для определения и проверки такого свойства.

Однако в наших тестах есть еще один недостаток: наши свойства недостаточно конкретны. Проверка того, что элемент начинается с Fizz или заканчивается Buzz , также удовлетворяется потоком, который просто содержит FizzBuzz для любого кратного 3 и 5. Можете ли вы исправить наши свойства?

Преимущества и недостатки

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

По сравнению с другими реализациями Vavr предоставляет хороший свободный API, который ненавязчив в отношении используемой среды тестирования. Работает с JUnit 4, JUnit 5 и TestNG. У Спока есть BDD, и я не уверен, имеет ли термин спецификация то же значение в Споке , что и в PBT, но это можно было бы изучить в другой статье. Кажется, это хорошее дополнение к подходу, основанному на данных.

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

Важнейшей частью PBT является то, насколько хорошо работает основной генератор случайных чисел (RNG) и насколько хорошо он засеян. Плохой ГСЧ приводит к неправильному распределению тестовых образцов, что может привести к «слепым зонам» тестируемого кода. Vavr полагается на ThreadLocalRandom и это более чем нормально.

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

Резюме

Мы видели, что тестирование на основе свойств основано на теории фальсифицируемости. Мы узнали, что у Vavr есть свободный API, который кодифицирует эту идею. Это позволяет нам определять свойства как комбинацию инварианта и генератора входных значений. По умолчанию инвариант проверяется на 1000 сгенерированных образцов.

Это позволило нам думать о FizzBuzz на более декларативном уровне. Мы определили и проверили три свойства и обсудили результаты.

Наконец, мы говорили о преимуществах и недостатках PBT.