- PVSM.RU - https://www.pvsm.ru -

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

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

Этот документ должен рассказать вам обо всем, что вам нужно, чтобы начать работу с hypothesis.

Пример

Предположим, мы написали run length encoding [1] систему, и хотим проверить, что она умеет.

У нас есть следующий код, который я взял прямо из Rosetta Code wiki [2] (ОК, я удалил какой-то прокомментированный код и исправил форматирование, но не модифицировал функции):

  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 [3].

Например, так:

virtualenv venv
source venv/bin/activate
pip install hypothesis

Создаст изолированную среду для вас, чтобы попробовать Hypothesis, не затрагивая установленные пакеты системы.

Выполнение тестов

В нашем примере выше мы просто позволяем pytest обнаружить и запустить наши тесты, но мы также могли бы запустить его явно сами:

  if __name__ == '__main__':
      test_decode_inverts_encode()

Или так unittest.TestCase [4]:

  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# [5], но по-прежнему очень полезна для помощи в поиске хороших идеи для использования Hypothesis.

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

Hypothesis Краткое руководство - 1 Обратно [7] Дальше [8] Hypothesis Краткое руководство - 2

Автор: AlekSandrDr

Источник [9]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/278625

Ссылки в тексте:

[1] run length encoding: https://en.wikipedia.org/wiki/Run-length_encoding

[2] Rosetta Code wiki: http://rosettacode.org/wiki/Run-length_encoding

[3] virtualenv: https://virtualenv.pypa.io/en/latest/

[4] unittest.TestCase: https://docs.python.org/2/library/unittest.html#unittest.TestCase

[5] Эта часть предназначена для реализации на F#: https://fsharpforfunandprofit.com/posts/property-based-testing-2/

[6] asking for help: https://hypothesis.readthedocs.io/en/latest/community.html

[7] Обратно: https://habrahabr.ru/post/354134/

[8] Дальше: https://habrahabr.ru/post/354146/

[9] Источник: https://habrahabr.ru/post/354144/?utm_campaign=354144