Статьи

Тестирование на основе свойств с помощью Спока

Тестирование на основе свойств — это альтернативный подход к тестированию, дополняющий тестирование на основе примеров . Последнее — это то, чем мы занимались всю жизнь: использование производственного кода на «примерах» — входные данные, которые мы считаем репрезентативными. Выбор этих примеров — это искусство само по себе: «обычные» входы, крайние случаи, искаженные входы и т. Д. Но почему мы ограничиваемся лишь несколькими примерами? Почему бы не проверить сотни, миллионы … ВСЕ входные данные? При таком подходе есть как минимум две трудности:

  1. Шкала. Чистая функция, принимающая только один ввод int , потребует 4 миллиардов тестов. Это означает несколько сотен гигабайт тестового исходного кода и несколько месяцев времени выполнения. Возведите в квадрат, если функция принимает два типа int . Для String это практически уходит в бесконечность.
  2. Предположим, у нас есть эти тесты, выполненные на квантовом компьютере или что-то в этом роде. Как вы знаете ожидаемый результат для каждого конкретного входа? Вы либо вводите его вручную (удачи), либо генерируете ожидаемый результат. Под генерацией я подразумеваю написать программу, которая выдает ожидаемое значение для каждого входа. Но разве мы не тестируем такую ​​программу в первую очередь? Предполагается ли нам писать лучшую, безошибочную версию тестируемого кода только для его тестирования? Также известный как уродливый зеркальный антипаттерн .

Итак, вы понимаете, что тестирование каждого входа, хотя и идеальное, это просто мысленный эксперимент, который невозможно осуществить. При этом тестирование на основе свойств пытается максимально приблизиться к этой нирване тестирования. Проблема № 1 решается с помощью тестируемого кода с сотнями или тысячами случайных входов. Не все, даже фракция. Но хорошее, случайное представление.

Выпуск № 2 на удивление сложнее. Тестирование на основе свойств может генерировать случайные аргументы, но не может выяснить, каким должен быть ожидаемый результат для этого случайного ввода. Таким образом, нам нужен другой механизм, дающий название всей философии. Нам нужно придумать свойства (инварианты, поведения), которые демонстрирует тестируемый код, независимо от того, что вводит. Это звучит очень теоретически, но в различных сценариях есть много таких свойств:

  1. Абсолютное значение любого числа никогда не должно быть отрицательным
  2. Кодирование и декодирование любой строки должны возвращать одну и ту же String для каждого симметричного кодирования.
  3. Оптимизированная версия какого-то старого алгоритма должна давать тот же результат, что и старый, для любого ввода
  4. Общая сумма денег в банке должна оставаться неизменной после произвольного числа внутрибанковских операций в любом порядке

Как вы можете видеть, есть много свойств, о которых мы можем думать, которые не упоминают конкретные примеры входных данных. Это не исчерпывающее и строгое тестирование. Это больше похоже на выборку и обеспечение того, чтобы образцы были «нормальными». Существует множество библиотек, поддерживающих тестирование на основе свойств практически для каждого языка. В этой статье мы рассмотрим Spock и ScalaCheck позже.

Спок + пользовательские генераторы данных

Спок не поддерживает готовое тестирование на основе свойств. Однако с помощью тестирования на основе данных и сторонних генераторов данных мы можем пойти довольно далеко. Таблицы данных в Spock могут быть обобщены в так называемые каналы данных :

01
02
03
04
05
06
07
08
09
10
11
12
def 'absolute value of #value should not be negative'() {
    expect:
    value.abs() >= 0
 
    where:
    value << randomInts(100)
}
 
private static def List<Integer> randomInts(int count) {
    final Random random = new Random()
    (1..count).collect { random.nextInt() }
}

.abs() выше код сгенерирует 100 случайных целых чисел и убедится, что для всех них .abs() неотрицателен. Вы можете подумать, что этот тест довольно тупой, но, к большому удивлению, он обнаружил одну ошибку! Но сначала давайте убьем некоторый шаблонный код. Генерация случайных входных данных, особенно более сложных, громоздка и скучна. Я нашел две библиотеки, которые могут помочь нам. Спок-генезис :

1
2
3
4
5
6
7
8
9
import spock.genesis.Gen
 
def 'absolute value of #value should not be negative'() {
    expect:
    value.abs() >= 0
 
    where:
    value << Gen.int.take(100)
}

Выглядит отлично, но если вы хотите сгенерировать, например, списки случайных целых чисел, net.java.quickcheck имеет более net.java.quickcheck API и не специфичен для Groovy:

01
02
03
04
05
06
07
08
09
10
import static net.java.quickcheck.generator.CombinedGeneratorsIterables.someLists
import static net.java.quickcheck.generator.PrimitiveGenerators.integers
 
def 'sum of non-negative numbers from #list should not be negative'() {
    expect:
    list.findAll{it >= 0}.sum() >= 0
 
    where:
    list << someLists(integers(), 100)
}

Этот тест интересный. Это гарантирует, что сумма неотрицательных чисел никогда не бывает отрицательной — генерируя 100 списков случайных чисел int s. Звучит разумно. Однако несколько тестов не проходят. Прежде всего из-за переполнения целого числа иногда два положительных целых числа складываются в отрицательное. Duh! Другой тип обнаруженного сбоя на самом деле пугающий. Хотя [1,2,3].sum() равно 6, очевидно, что [].sum() равно … null ( WAT? )

Как видите, даже самые глупые и базовые тесты на основе свойств могут быть полезны при поиске необычных угловых случаев в ваших данных. Но подождите, я сказал, что тестирование absolute of int обнаружило одну ошибку. На самом деле это не так, из-за плохих (слишком «случайных») генераторов данных, которые не возвращали известные граничные значения. Мы исправим это в следующей статье .

Ссылка: Основанное на свойствах тестирование со Споком от нашего партнера JCG Томаша Нуркевича в блоге Java и соседстве .