Краткое руководство
Эта статья является переводом страницы Hypothesis — Quick start guide взятой из официального руководства.
Я не смог найти какой то полезной информации на русском языке по использованию Гипотезы, кроме выступления 23 ноября 2017 г. Александра Шорина на "Moscow Python Meetup 50". Решил разобраться. В итоге что то перевел. Вот, решил поделиться.
Этот документ должен рассказать вам обо всем, что вам нужно, чтобы начать работу с hypothesis.
Пример
Предположим, мы написали run length encoding систему, и хотим проверить, что она умеет.
У нас есть следующий код, который я взял прямо из Rosetta Code wiki (ОК, я удалил какой-то прокомментированный код и исправил форматирование, но не модифицировал функции):
def encode(input_string): count = 1 prev = '' lst = [] for character in input_string: if character != prev: if prev: entry = (prev, count) lst.append(entry) count = 1 prev = character else: count += 1 else: entry = (character, count) lst.append(entry) return lst def decode(lst): q = '' for character, count in lst: q += character * count return q
Мы хотим написать тест для этой пары функций, который проверит некоторый инвариант из их должностных обязанностей.
Инвариант, когда у вас есть такого рода encoding/decoding заключается в том, что если вы кодируете что-то, а затем декодируете это, то получаете то же самое значение назад.
Давайте посмотрим, как это можно сделать с помощью Hypothesis:
from hypothesis import given from hypothesis.strategies import text @given(text()) def test_decode_inverts_encode(s): assert decode(encode(s)) == s
(Для этого примера мы просто позволим pytest обнаружить и запустить тест. О других способах, которыми вы могли бы запустить его, мы расскажем позже).
Функция text возвращает то, что Hypothesis называет стратегией поиска. Объект с методами которые описывают, как произвести и упростить некоторые виды значений. Затем декоратор @given берет наш тест функции и превращает его в параметризованный, который при вызове будет выполнять тестовую функцию по широкому диапазону совпадающих данных из этой стратегии.
Во всяком случае, этот тест сразу находит ошибку в коде:
Falsifying example: test_decode_inverts_encode(s='') UnboundLocalError: local variable 'character' referenced before assignment
Прим: Локальная переменная character, упоминается до присвоения
Hypothesis правильно указывает на то, что этот код просто неправильный, если он вызван для пустой строки.
Если мы исправим это, просто добавив следующий код в начало функции, тогда Hypothesis скажет нам, что код правильный (ничего не делая, как вы и ожидали проходя тест).
if not input_string: return []
Если бы мы хотели убедиться, что этот пример всегда будет проверяться, мы могли бы добавить его явно:
from hypothesis import given, example from hypothesis.strategies import text @given(text()) @example('') def test_decode_inverts_encode(s): assert decode(encode(s)) == s
Вам не обязательно этого делать, но это может быть полезно: как для ясности, так и для надежного поиска примеров. Также в рамках локального "обучения", в любом случае, Hypothesis будет помнить и повторно использовать примеры, но вот для обмена данными в вашей системе непрерывной интеграции (CI) в настоящее время нет приемлемого хорошего рабочего процесса.
Также стоит отметить, что аргументы ключевых слов example, и given могут быть как именованными, так и позиционными. Следующий код сработал бы так же хорошо:
@given(s=text()) @example(s='') def test_decode_inverts_encode(s): assert decode(encode(s)) == s
Предположим, у нас была более интересная ошибка и мы забыли перезагрузить счетчик
в цикле. Скажем, мы пропустили строку в нашем методе encode:
def encode(input_string): count = 1 prev = '' lst = [] for character in input_string: if character != prev: if prev: entry = (prev, count) lst.append(entry) # count = 1 # Отсутствует операция сброса prev = character else: count += 1 else: entry = (character, count) lst.append(entry) return lst
Hypothesis быстро проинформирует нас в следующем примере:
Falsifying example: test_decode_inverts_encode(s='001')
Обратите внимание, что представленный пример действительно очень прост. Hypothesis не просто
находит любой попавшийся пример для ваших тестов, он знает, как упростить примеры
которые он находит для создания маленьких и легко понятных. В этом случае два идентичных
значений достаточно, чтобы установить счетчик на число, отличное от одного, за которым следует
другое значение, которое должно было бы сбросить счет, но в этом случае
не сделало.
Примеры Hypothesis представляют собой действительный код Python, который вы можете запустить. Любые аргументы, которые вы явно указываете при вызове функции, не генерируются Hypothesis-ом, и если вы явно предоставляете все аргументы, Hypothesis просто вызовет базовую функцию один раз, а не будет запускать ее несколько раз.
Установка
Hypothesis является available on pypi as "hypothesis". Вы можете установить его с помощью:
pip install hypothesis
Если вы хотите установить непосредственно из исходного кода (например, потому что вы хотите
внести изменения и установить измененную версию) вы можете сделать это с:
pip install -e .
Вы, вероятно, должны сначала запустить тесты, чтобы убедиться, что ничего не сломано. Вы можете сделать это так:
python setup.py test
Обратите внимание, что если они еще не установлены, будет предпринята попытка установить тестовые зависимости.
Вы можете сделать все это в virtualenv.
Например, так:
virtualenv venv source venv/bin/activate pip install hypothesis
Создаст изолированную среду для вас, чтобы попробовать Hypothesis, не затрагивая установленные пакеты системы.
Выполнение тестов
В нашем примере выше мы просто позволяем pytest обнаружить и запустить наши тесты, но мы также могли бы запустить его явно сами:
if __name__ == '__main__': test_decode_inverts_encode()
Или так unittest.TestCase:
import unittest class TestEncoding(unittest.TestCase): @given(text()) def test_decode_inverts_encode(self, s): self.assertEqual(decode(encode(s)), s) if __name__ == '__main__': unittest.main()
Примечание: это работает, потому что Hypothesis игнорирует любые аргументы, которые ему не было сказано предоставить (позиционные аргументы начинаются справа), поэтому аргумент self для теста просто игнорируется и работает как обычно. Это также означает, что Hypothesis будет хорошо играть с другими способами параметризации тестов. Например, он отлично работает, если вы используете приспособления pytest для некоторых аргументов и Hypothesis для других.
Написание тестов
Тест в Hypothesis состоит из двух частей: функции, которая выглядит как обычный тест в выбранной тестовой структуре, но с некоторыми дополнительными аргументами, и декоратора @given, который указывает, как предоставить эти аргументы.
Вот некоторые другие примеры того, как можно это использовать:
from hypothesis import given import hypothesis.strategies as st @given(st.integers(), st.integers()) def test_ints_are_commutative(x, y): assert x + y == y + x @given(x=st.integers(), y=st.integers()) def test_ints_cancel(x, y): assert (x + y) - y == x @given(st.lists(st.integers())) def test_reversing_twice_gives_same_list(xs): # Это создаст списки произвольной длины (обычно между 0 и # 100 элементами), элементы которых являются целыми числами. ys = list(xs) ys.reverse() ys.reverse() assert xs == ys @given(st.tuples(st.booleans(), st.text())) def test_look_tuples_work_too(t): # Кортеж создается как тот, который вы предоставили, с # соответствующими типами в этих позициях. assert len(t) == 2 assert isinstance(t[0], bool) assert isinstance(t[1], str)
Обратите внимание, что, как мы видели в приведенном выше примере, вы можете передать аргументы @given как позиционные или именованные.
С чего начать
Теперь вы знаете достаточно об основах, чтобы написать какие то тесты для вашего кода с помощью Hypothesis. Лучший способ учиться — это сделать, так что попробуйте.
Если у вас туговато с идеями о том, как использовать этот вид теста для вашего кода, вот несколько подсказок:
- Попробуйте просто вызвать функции с соответствующими случайными данными и получите в них сбой. Вы возможно будете удивлены, как часто это работает. Например, обратите внимание, что первая ошибка, которую мы обнаружили в примере кодирования, даже не дошла до нашего утверждения: она потерпела крах, потому что она не смогла обработать данные, которые мы дали, а не потому, что произошло что то не правильное.
- Поищите дублирование в своих тестах. Есть ли случаи, когда вы тестируете одно и то же с несколькими разными примерами? Можете ли вы обобщить это в один тест, используя Hypothesis?
- Эта часть предназначена для реализации на F#, но по-прежнему очень полезна для помощи в поиске хороших идеи для использования Hypothesis.
Если у вас возникли проблемы с запуском, не стесняйтесь и обращайтесь asking for help .

