Краткое руководство


Эта статья является переводом страницы 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. Лучший способ учиться — это сделать, так что попробуйте.


Если у вас туговато с идеями о том, как использовать этот вид теста для вашего кода, вот несколько подсказок:


  1. Попробуйте просто вызвать функции с соответствующими случайными данными и получите в них сбой. Вы возможно будете удивлены, как часто это работает. Например, обратите внимание, что первая ошибка, которую мы обнаружили в примере кодирования, даже не дошла до нашего утверждения: она потерпела крах, потому что она не смогла обработать данные, которые мы дали, а не потому, что произошло что то не правильное.
  2. Поищите дублирование в своих тестах. Есть ли случаи, когда вы тестируете одно и то же с несколькими разными примерами? Можете ли вы обобщить это в один тест, используя Hypothesis?
  3. Эта часть предназначена для реализации на F#, но по-прежнему очень полезна для помощи в поиске хороших идеи для использования Hypothesis.

Если у вас возникли проблемы с запуском, не стесняйтесь и обращайтесь asking for help .


Обратно Дальше

Комментарии (0)