Hypothesis: тестирование с помощью property-based testing | OTUS
🔥 Начинаем BLACK FRIDAY!
Максимальная скидка -25% на всё. Успейте начать обучение по самой выгодной цене.
Выбрать курс

Курсы

Программирование
iOS Developer. Basic
-25%
Python Developer. Professional
-25%
Разработчик на Spring Framework
-25%
Golang Developer. Professional
-25%
Python Developer. Basic
-25%
iOS Developer. Professional
-25%
Node.js Developer
-25%
Unity Game Developer. Professional
-25%
React.js Developer
-25%
Android Developer. Professional
-25%
Software Architect
-25%
C++ Developer. Professional
-25%
Backend-разработчик на PHP Web-разработчик на Python Алгоритмы и структуры данных Framework Laravel PostgreSQL Team Lead Разработчик голосовых ассистентов и чат-ботов Архитектура и шаблоны проектирования Agile Project Manager Нереляционные базы данных Супер - интенсив по паттернам проектирования Супер-практикум по использованию и настройке GIT IoT-разработчик Подготовка к сертификации Oracle Java Programmer (OCAJP) Супер-интенсив «СУБД в высоконагруженных системах» Супер-интенсив "Azure для разработчиков"
Инфраструктура
Мониторинг и логирование: Zabbix, Prometheus, ELK
-25%
DevOps практики и инструменты
-25%
Архитектор сетей
-25%
Инфраструктурная платформа на основе Kubernetes
-25%
Супер-интенсив «ELK»
-16%
Супер-интенсив «IaC Ansible»
-16%
Administrator Linux. Professional MS SQL Server Developer Безопасность Linux PostgreSQL Reverse-Engineering. Professional CI/CD VOIP инженер Супер-практикум по работе с протоколом BGP Супер - интенсив по паттернам проектирования Супер - интенсив по Kubernetes Administrator Linux.Basic Супер-интенсив "Tarantool"
Специализации Курсы в разработке Подготовительные курсы
+7 499 938-92-02

Hypothesis: тестирование с помощью property-based testing

В данной статье мы поговорим о том, что такое property-based testing, какие тесты с его помощью писать особенно эффективно, и может ли он заменить всё юнит-тестирование.

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

Но если мы используем Python и наша функция принимает на вход один параметр типа int, то придется тестировать значения от -sys.maxint + 1 до sys.maxint, где

>>> sys.maxint
9223372036854775807

при использовании Python2. В Python3 int вообще не имеет границ, и протестировать вообще все значения не получится, даже если не обращать внимание на конечность ресурсов. Но даже в Python2 тестирование всех возможных вариантов займет очень много времени. А ведь это только "позитивные" тест-кейсы, а есть еще негативные, например, что будет, если мы подадим float, или строку, или список. Если используется язык со строгой типизацией, этот список будет уже, но и там бывает нужно протестировать NaN или другие "особенные" числа.

Естественное желание протестировать все возможные варианты проходит, если познакомиться с техниками тест-дизайна. Они помогают выделить главные случаи, чтобы включать в набор тестов, то есть будут прогоняться те, которые действительно могут быть полезны. Но, используя только значения, о которых мы думаем, без дополнительной тренировки довольно просто упустить какой-то важный случай. Тогда на помощь приходит тестирование случайных данных или эмулирующие случайные действия пользователя такие как fuzzy-testing и monkey-testing.

Fuzz-testing -- техника, при которой тестируемое программное обеспечение нагружается большими объемами случайно сгенерированной информации. Monkey-testing -- техника, при которой пользователь (или скрипт, его эмулирующий) случайным образом пользуется программным обеспечением: отправляет случайные данные в поля пользовательского ввода, может запрашивать данные, которые еще не существуют в системе, и так далее.

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

Однако и без глубокого знания тест-дизайна можно улучшить свои тесты. В том числе с помощью библиотек, которые реализуют property-based testing-подход.

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

Кроме этого, для удобства использования тест должен развалиться на "минимальном" возможном варианте. То есть если мы тестируем функцию, которая не работает на числах, которые делятся на 25, но не делятся на 100, тест развалится на числе 25, а не, например, 225. Эта функциональность помогает быстрее найти ошибку, потому что чем "проще" выглядит результат, на котором разваливается тест, тем зачастую проще понять, какими именно свойствами обладает этот пример, и что именно идет не так в коде.

Немаловажно также, что property-based-подход гарантирует воспроизводимость тест-кейса, что не всегда происходит, если мы просто будем запускать тест со случайным значением параметра.

Благодаря этим фичам, property-based testing стал популярен среди разработчиков.

Property-based-подход был впервые придуман Джоном Хьюзом и реализован на языке Haskel в библиотеке QuickCheck. На сегодняшний день реализации этого подхода есть на многих других языках. Реализация property-based testing на Python называется [hypothesis] (https://github.com/HypothesisWorks/hypothesis), более полный список языков, для которых есть подобные библиотеки, можно посмотреть по ссылке

В Hypothesis реализованы так называемые стратегии для разных типов объектов, они включают в себя как стандартные объекты Python, такие как строки, целые числа, логические типы, так и возможность построить персонализированные, например, строки, подчиняющиеся какому-то регулярному выражению.

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

Посмотрим, для каких случаев property-based-подход будет наиболее эффективен.

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

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

def func(a, b):
    return a*b

func(1, b) = b

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

Часто функции, которые мы пишем, идемпотенты. Это значит, что при применении функции к результату примененной функции, результат не меняется. Например:

def func(s):
    return s.upper()

func(some_string) = func(func(some_string))

Тесты, написанные с помощью подхода property-based хорошо подходят для проверки таких свойств.

Описанные выше примеры можно обобщить, как проверку каких-то свойств, которые сохраняются при определенных условиях. Но это не единственный тип тестов, для написания которых удобно использовать proprty-based-библиотеки.

Описанные далее характерные черты также являются отличными применениями для них.

Метод "Хоббита"

Тестирование пути туда и обратно, которое также иногда называют "Методом Хоббита". Например, мы должны протестировать функцию шифрования пароля. Но если у нас есть функция шифрования, наверняка есть и функция для дешифрования. И после дешифровки зашифрованного объекта мы должны получить первоначальный объект:

def encript(obj):
    ...

def decript(pwd):
    ...

obj = decript(encript(obj))

Также этот способ тестирования можно применить для тестирования сериализации-десериализации объектов.

Тесты, написанные с помощью Hypothesis и других property-based-библиотек, очень хорошо подходят для тестирования функционала, в котором устанавливаются или получаются какие-то атрибуты классов. Здесь это полезно, потому что это именно то место, где нужно экстенсивно тестировать различные возможности входных данных. Это поможет лучше подготовиться к различным вариантам пользовательского ввода, который может сильно отличаться от того, что ожидает разработчик.

Свойства, которые легко проверить, но сложно доказать делают задачу подходящей для property-based-подхода. Такие как, например, сортировка: легко проверить, что элементы массива упорядочены, но сам алгоритм сортировки может быть довольно сложным, и тестировать правильность каждого шага часто неэффективно. Однако если мы подадим на вход большое количество разнообразных массивов, и наша функция сортировки отработает без проблем, то можно будет достаточно достоверно сказать, что функция работает. Аналогичные рассуждения применимы для таких функций, как токенизация: мы можем легко проверить, что количество токенов в результате получилось таким же, как количество разделителей в первоначальном тексте.

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

Нужно отметить, что тесты, созданные с помощью hypothesis, пока не могут заменить полностью все остальное юнит-тестирование. Во-первых, потому что поиск свойств, которые могут породить тест-кейсы для hypothsis, -- это отдельная нетривиальная задача, и она не всегда может решаться для конкретного приложения. Даже если найдены свойства, на которых может быть основано большое количество тестов, полное покрытие кода никак не гарантировано. И, конечно же, они не являются заменой для интеграционных и end-to-end тестов, потому что тестируют более низкий уровень приложения. Но они являются хорошим дополнением к тестам и помогут протестировать случаи, о которых легко забыть или не обратить внимание.

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
🎁 Максимальная скидка!
Черная пятница уже в OTUS! Скидка -25% на всё!