Статьи

Учебное пособие по автоматическому тестированию на основе свойств

Я большой сторонник автоматизированного тестирования и выяснения того, что ваш код не работает на вашем компьютере через 30 секунд после его написания, а не в производстве после того, как он привел к денежным убыткам и выполнению некоторых ремонтных работ. Это относится ко многим различным видам тестирования, от уровня подразделения (который также имеет преимущества для внутреннего качества и обратной связи при проектировании) до уровня приемлемости (который обеспечивает заинтересованные стороны тем, что им нужно, и документирует это на будущее). Тестируемая система может быть отдельным классом, проектом или даже совместной (микро) службой, доступ к которой осуществляется через HTTP из другого процесса или машины. 

Однако классические тестовые наборы, написанные с использованием стилей xUnit и BDD, имеют некоторые проблемы с масштабированием, с которыми они сталкиваются, когда вы хотите использовать больше, чем просто удачные пути:  

  • Это  трудно охватить множество различных входов с  помощью ручного написания тестов, поэтому мы придерживаться не более десятка случаев для конкретного метода.
  • Для каждого нового входа, который мы хотим протестировать, существуют  затраты на обслуживание  : каждому необходимо  написать некоторые  утверждения и обновить их в будущем, если тестируемая система изменит свой API.
  • Мы  склонны не проверять внешние зависимости,  такие как язык или библиотеки, поскольку доверяем им, чтобы они хорошо справлялись с работой, даже если мы несем ответственность за их ошибки. Мы выбрали их и разворачиваем наш проект, а не авторов, которые предоставили код  «как есть», без гарантии.

Обратите внимание, что здесь я определяю  входные данные  как существующее состояние системы плюс новые входные данные, предоставленные тестом (данные сценария «Когда» и «Когда»), и  выводим  не только фактический ответ, создаваемый тестируемой системой, но и новый Государство это приняло.

Сортировать()

Давайте возьмем в качестве примера функцию sort (), которую сегодня никто не реализует, за исключением собеседований и заданий.

Предполагая массив (или список, в зависимости от вашего языка), мы можем создать несколько входных данных для функции, как мы это сделали бы в ката:

  • [1, 2, 3]
  • [3, 1]
  • [3, 6, 5, 1, 4]

и так далее. Когда мы остановимся? Может быть, нам также нужно немного сложного ввода:

  • []
  • [1]
  • [1, 1]
  • [2, 3, 5, 6, 8, 9, 1, 3, 6, 7, 8, 9]

После того, как мы собрали все входные данные, нам нужно определить, что мы ожидаем от каждого из них:

  • [1, 2, 3] => [1, 2, 3]
  • [3, 1] => [1, 3 [
  • [3, 6, 5, 1, 4] => [1, 3, 4, 5, 6]
  • [] => []
  • [1] => [1]
  • [1, 1] => [1, 1]
  • [2, 3, 5, 6, 8, 9, 1, 3, 6, 7, 8, 9] => [1, 2, 3, 3, 5, 6, 6, 7, 8, 8, 9, 9]

Вы можете делать это постепенно, увеличивая код по одному новому тесту за раз, но вам все равно придется это делать. Учитывая, что это может стать скучным и подверженным ошибкам в этом игрушечном примере, вы задаетесь вопросом, что делать, когда вместо sort () у вас есть класс RangeOfMoney, который является ключевым для вашего бизнеса и управляет точечными и произвольными интервалами денежных сумм (правдивая история) ).

Тестирование на основе свойств в двух словах

Тестирование на основе свойств в подходе к тестированию из мира функционального программирования. Чтобы решить вышеупомянутые проблемы (и получить новые, более интересные), необходимо выполнить следующие шаги:

  1. Генерация  случайной выборки возможных входных данных.
  2. Упражнение  SUT с каждым из них.
  3. Проверяйте свойства,  которые должны быть истинными для каждого вывода, вместо того, чтобы делать точные сравнения.
  4. (Необязательно), если проверка свойств не удалась, возможно, сожмите, чтобы найти минимальный ввод, который все еще вызывает сбой.

Как это работает для функции sort ()?

Мы можем использовать rand () для генерации входного массива: 

Generator\seq(
    Generator\nat(),
    Generator\pos(100)
)

Этот массив состоит из натуральных чисел ( Gen \ nat ) и имеет длину до 100 элементов ( Gen \ pos (100) ), поскольку очень длинные массивы могут замедлить наши тесты.

Затем для каждого из этих входов мы выполняем sort () и проверяем простое свойство на выходе, которое является порядком элементов:

sort($array);
for ($i = 0; $i < count($array) - 1; $i++) {
    $this->assertTrue(
        $array[$i] <= $array[$i+1],
        "Array is not sorted: " . var_export($array, true)
    );
}

Это не единственное свойство, которое поддерживает sort (), но это первое, что я хотел бы указать. Возможны и другие:

  • каждый элемент на входе также на выходе
  • каждый элемент на выходе также находится на входе
  • длина входного и выходного массивов одинакова.

Вот  полный пример, написанный с использованием Eris , нашего расширения для PHPUnit, которое автоматизирует генерацию множества видов случайных данных и их проверку.

Как найти недвижимость?

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

Некоторые практические правила для определения свойств:

  • ищите  обратные функции  (например, сложение и вычитание, или удвоение размера изображения и уменьшение его до 50%). Вы можете использовать инверсию на выходе и проверить равенство с входом.
  • Соотнесите  ввод и вывод с некоторым свойством true или false для обоих  (например, в примере sort (), чем элемент в одном из двух массивов также в другом)
  • Определите  почтовые условия и инварианты,  которые всегда выполняются в конкретной ситуации (например, в примере sort (), что выходные данные отсортированы, но в целом вы можете ограничить возможные выходные значения функции очень сильно, говоря, что это массив, он содержит только целые числа, его длина равна длине ввода.)

[2, 3, 5, 6, 8, 9, 1, 3, 6, 7, 8, 9] проваливают мой тест

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

Это та же самая работа, которую мы делаем при открытии отчета об ошибке для чужого кода: мы пытаемся найти минимальную комбинацию, которая вызывает ошибку, чтобы отбросить все ненужные детали, которые замедлили бы ее исправление.

Таким образом, при тестировании на основе свойств [2, 3, 5, 6, 8, 9, 1, 3, 6, 7, 8, 9], вероятно, можно сократить до [2, 3, 5, 6, 8, 9, 1, 3, 6, 7, 8] и, возможно, до [1, 0], в зависимости от ошибки. Этот процесс завершается попыткой уменьшить все сгенерированные случайные значения, которые в нашем случае были длиной массива и содержащимися значениями.

Тестирование языка

Итак, вот код, который я ожидаю работать:

function fromZeroBasedDayOfYear($year, $dayOfYear)
{
    return DateTime::createFromFormat(
        'z Y H i s',
        $dayOfYear . ' '. $year . ' 00 00 00',
        new DateTimeZone("UTC")
    ); 
}

Эта функция создает экземпляр PHP DateTime с использованием собственного   расширения datetime , которое является стандартом для мира PHP. Он начинается с года и номера дня в диапазоне от 0 до 364 (или 365) и строит DateTime, указывающее на полночь этого конкретного дня.

Вот основанный на свойствах тест для этой функции:

$this->forAll(
    Generator\int(2000, 2020),
    Generator\int(0, 364),
    Generator\int(0, 364)
)
->then(function($year, $dayOfYear, $anotherDayOfYear) {
    $day = fromZeroBasedDayOfYear($year, $dayOfYear);
    $anotherDay = fromZeroBasedDayOfYear($year, $anotherDayOfYear);
    $this->assertEquals(
        abs($dayOfYear - $anotherDayOfYear) * 86400,
        abs($day->getTimestamp() - $anotherDay->getTimestamp()),
        "Days of the year $year: $dayOfYear, $anotherDayOfYear" . PHP_EOL
        . "{$day->format(DateTime::ISO8601)}, {$anotherDay->format(DateTime::ISO8601)}"
    );
});

Мы генерируем два случайных целых числа в [0. 364] и проверить, что разница в секундах двух сгенерированных объектов DateTime равна 86400 секундам, умноженным на число дней, прошедших между двумя выбранными датами. Свойство ввода (расстояние) сохраняется над выводом в другой форме (секунды, а не дни).

Удивительно, но  этот тест  не проходит со следующим сообщением:

Time: 95 ms, Memory: 6.00Mb

There was 1 failure:

1) DateTest::testFromDayOfYearFactoryMethodRespectsDistanceBetweenDays
Days of the year 2016: 59, 0
2016-03-01T00:00:00+0000, 2016-01-01T00:00:00+0000
Failed asserting that 5184000 matches expected 5097600.

/home/giorgio/code/eris/examples/DateTest.php:53
/home/giorgio/code/eris/src/Eris/Quantifier/Evaluation.php:46
/home/giorgio/code/eris/src/Eris/Shrinker/Random.php:67
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:82
/home/giorgio/code/eris/src/Eris/Quantifier/Evaluation.php:48
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:84
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:123
/home/giorgio/code/eris/examples/DateTest.php:54
/home/giorgio/code/eris/examples/DateTest.php:54
                                       
FAILURES!                              
Tests: 1, Assertions: 346, Failures: 1.

Произошло то, что мы вызвали  ошибку объекта DateTime  при его создании с определенной комбинацией формата и часового пояса. Чистый эффект этой ошибки мог состоять в том, что наши финансовые отчеты (показывающие ежедневную выручку) начали бы показывать неправильные цифры, начиная с 29 февраля следующего года.

Обратите внимание, что ввод сокращается до простейших возможных значений, которые вызывают проблему: 1 января для одного значения и 1 марта для другого.

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

В заключении

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

Учитывая сложность домена, которую обрабатывает   расширение datetime , оно делает фантастическую работу и разрабатывается очень компетентными программистами. Тем не менее, если они могут ошибиться, я верю, что и мой собственный код тоже. Тестирование на основе свойств — это дополнительный инструмент, который может работать параллельно с тестированием на основе примеров для выявления проблем в наших проектах.

Мы назвали  расширение PHPUnit на основе свойств в честь Эрисы , греческой богини хаоса, поскольку серьезное тестирование означает атаку на ваш код и на платформу, на которой он построен, в попытке взломать его раньше, чем кто-либо другой.

Ссылки