Содержание
- Прежде чем мы начнем
- Тестирование на основе свойств
- Пример обратного списка
- Проверка недвижимости FizzBuzz
- Каждый третий элемент должен начинаться с Fizz
- Увеличить размер выборки
- Каждый кратный из 5 должен заканчиваться Buzz
- Каждый не третий и не пятый элемент должен оставаться числом
- Другие свойства
- Преимущества и недостатки
- Резюме
- Комментарии
Обычное модульное тестирование основано на примерах: мы определяем вход для нашего модуля и проверяем ожидаемый результат. Таким образом, мы можем гарантировать, что для конкретного примера наш код работает правильно. Однако это не доказывает правильность для всех возможных входных данных. Тестирование на основе свойств, также называемое проверкой свойств, использует другой подход, придерживаясь научной теории фальсифицируемости : до тех пор, пока нет доказательств того, что предлагаемое свойство нашего кода является ложным, оно считается истинным. В этой статье мы используем 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 мс.
Увеличить размер выборки
По умолчанию 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.