Рубрики
Без рубрики

Введение в развитие, ориентированное на свойства

Узнайте, что такое развитие, ориентированное на недвижимость и как применить его для проекта. Теги от тестирования, учебника, начинающими, Python.

Эта статья была отредактирована Каролин Странский и вдохновлен Тестирование на основе недвижимости с правильным, Erlang и Elixir книга.

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

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

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

Что в этом руководстве

  • Что такое развитие, ориентированное на свойства?
  • Пример проекта: Сортированный словарь
  • Вывод
  • Дополнительные ресурсы

⚠️ Предпосылки :

  • Общее понимание программного тестирования.
  • (Необязательно) Python 3+ * Если вы хотите следовать.

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

💻 Ссылки :

Это Репозиторий GitHub Содержит все показанные примеры кода в качестве тестов. Репозиторий также содержит инструкции для их выполнения.

Что такое развитие, ориентированное на свойства?

Как уже упоминалось выше, развитие, ориентированное на недвижимость Тестовое развитие с Испытания на основе недвижимости Отказ Разработка, ориентированная на тестирование, просит нас подумать, что наш код должен делать и поставить это в тест. В контексте этой статьи не стоит ли мы написать наши тесты до, после, или во время реализации. Что важно, так это то, что тесты направляют развитие. Тестирование на основе недвижимости просит нас сформулировать эти тесты как свойства Отказ

Например, скажем, мы пишем код для преобразования CSV в JSON множество. Вместо того, чтобы прыгать в писать анализатор CSV, развитие, управляемое тестированием, просит нас впервые придумать тестовые случаи.

Вот пример ввода CSV:

a,b,c
3,2,1
6,3,2

Это должно быть превращено в следующие JSON:

[
  { "a": 3, "b": 2, "c": 1 },
  { "a": 6, "b": 3, "c": 2 }
]

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

  • Ключи отличаются.
  • Значения целые числа.
  • Там нет отсутствующих ценностей.

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

Тесты на основе недвижимости отлично подходят для того, чтобы заставить нас быть явна о наших предположениях Отказ Чтобы придумать свойства для нашего парсера CSV-To-JSON, нам нужно сначала генерировать CSV, мы ожидаем, что наш парсер сможет справиться.

Вот псевдокод для этого типа генератора:

  1. Создание клавиш: список произвольных струн.
  2. Создайте количество строк: неотрицательное целое число.
  3. Создание строк: для каждой строки и ключа генерируйте произвольную строковое значение.

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

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

Генератор, который мы создали, является примером подхода снизу вверх к генерации данных. Вместо того, чтобы генерировать случайные CSVS напрямую, мы создаем их шагом за шагом и отслеживать, что идет. Поэтому, когда придет время написать утверждение, что наш код сделал правильно, мы знаем, что такое ожидаемый результат. Это позволяет избежать проблемы, в которой нам нужно дублировать возможно сложное реализацию в тестах. Например, с нашим генератором мы знаем, что длина полученного массива JSON должна быть равна количеству строк, генерируемых на шаге 2. Это хорошая недвижимость! Если вы хотите погрузиться более глубоко, как применить тестирование на основе недвижимости к разбору CSV, посмотрите на Эта глава от Правильная тестовая книга Отказ

С этим мы должны иметь понимание того, что такое развитие, ориентированное на свойства. Далее давайте применяем эти принципы в примере проекта.

Пример проекта: Сортированный словарь

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

Для нашего проекта мы сохраним ключи отсортированными, сохраняя пары клавишных значений в Двоичное дерево поиска Отказ

Примечание. Поскольку стандартное дерево может быть неуравновешенным, и, следовательно, неэффективным, вы должны использовать SortedContainers В производственном программном обеспечении.

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

Чтобы реализовать двоичное дерево поиска, мы приберем к справочным алгоритмам Введение в алгоритмы Учебник.

Ради этой статьи мы предположим, что ключи целые числа и что сами ключей используются для сравнения (вместо того, чтобы предоставить пользовательскую Callable за счет как SortedDict в SortedContainers It).

Что это должно делать?

Чтобы придумать свойства, мы сначала должны придумать требования к нашему Сортируется Отказ Один из способов придумать требования – это играть с API, который мы ожидаем, что наш модуль.

Во-первых, мы ожидаем, что мы можем найти вставленный ключ:

>>> # Insert and search
>>> sorted_dict = SortedDict()
>>> sorted_dict[2] = 'two'
>>> sorted_dict[2]
'two'

Мы также ожидаем, что ключи всегда отсортированы независимо от заказа в вставку. Продолжение предыдущего примера:

>>> # Keys are sorted
>>> sorted_dict[1] = 'one'
>>> sorted_dict.keys()
[1, 2]

Как и со стандартным словарем в Python, мы ожидаем, что вновь вставка существующего ключа приведет к перезаписанию стоимости:

>>> # Handles duplicate keys
>>> sorted_dict[2] = 'two-two'
>>> sorted_dict[2]
'two-two'

Мы ожидаем, что поиск несуществующего ключа повысит KeyError :

>>> # Non-existing key
>>> sorted_dict[3]
Traceback (most recent call last):
KeyError: ...

Примечание: ... представляет остальные KeyError бревно.

Наконец, если мы удалим ключ, мы ожидаем, что поиск его также повысит KeyError :

>>> # Searching for deleted key
>>> del sorted_dict[1]
>>> sorted_dict[1]
Traceback (most recent call last):
KeyError: ...

Листинг требований

Теперь, когда у нас есть представление о том, что мы ожидаем, что наш код, мы можем попытаться обобщить эти предыдущие примеры в требованиях:

  1. Пары клавиш, добавленные в наш сортирный словарь, можно найти.
  2. Добавление ключа, которое уже существует, перезаписывает существующий.
  3. Ключи всегда отсортированы.
  4. Поиск несуществующего ключа повышает KeyError Отказ
  5. Удаление ключа, а затем ищет его поднимает KeyError Отказ

Эти требования будут служить основой для наших свойства Отказ

Далее мы напишем недвижимость на первое требование: возможность вставить пары клавишных пар в словарь, а затем искать их.

Внедрение вставки и поиска

Перед записью тестовых генераторов мы впервые добавим основной скелет для Сортируется :

# sorted_dict.py
class SortedDict:
    def __init__(self):
        """
        Sorted dictionary, keeps key-value pairs
        sorted by the key.
        """
        pass

    def __setitem__(self, key, item):
        """
        Add a key-value pair to the dictionary.
        """
        pass

    def __getitem__(self, key):
        """
        Get a key-value pair from the dictionary.
        """
        pass

__setitem__ Метод называется, когда Сортировать_dict [ключ] вызывается. Точно так же __getitem__ Определяет поведение для Сортировать_dict [ключ] Отказ

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

Генератор

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

Чтобы начать с написания нашего первого теста на основе недвижимости, нам нужен генератор ключевого значения кортежи . Точка входа к генераторам в гипотезе – Гипотеза.стратегии Модуль, который мы псевдоним как некоторые ниже, потому что он хорошо читает. Стратегии гипотезы являются по сути «умные генераторы данных», которые вы можете составить для генерации очень сложных данных.

Вот как составить генераторы в гипотезе для создания списка кортежей ключей:

# test_sorted_dict.py
import hypothesis.strategies as some

def some_key_value_tuples():
    """
    Generator for lists of key-value tuples.
    """
    some_keys = some.integers()
    some_values = some.binary()
    some_kvs = some.tuples(some_keys, some_values)
    return some.lists(some_kvs)

Функция сначала определяет генератор quey_keys Для ключей, которые мы предполагаем, целые числа. Для значений мы предполагаем, что любой двоичный действительно действителен. Затем мы определяем que_kvs , генератор кортежей ключей, используя uvery_keys. и que_values как генераторы клавиш и ценностей соответственно. Наконец, мы возвращаем генератор quot.blists (que_kvs) , который генерирует списки кортежей.

Чтобы увидеть, какие данные генератор создает, мы можем позвонить uvere_key_value_tupes (). Пример () :

>>> some_key_value_tuples().example()
[]
>>> some_key_value_tuples().example()
[(53, b'{\x8b\xed\x92\xa8\xcb\x7fq\x95')]
>>> some_key_value_tuples().example()
[(-19443, b'\x16ERa'), (-425, b'')]

Теперь, когда у нас есть генератор для списков кортежей ключей, мы хотим построить генераторные экземпляры Сортируется содержащий эти кортежи. С гипотезой мы можем использовать Hypothesese.strategies.Composite Для этого следующим образом:

# test_sorted_dict.py
@some.composite
def some_sorted_dicts(draw):
    """
    Generator of sorted dicts.
    """
    key_values = draw(some_key_value_tuples())

    sorted_dict = SortedDict()
    for key, val in key_values:
        sorted_dict[key] = val
    return sorted_dict

@ Некоторые счастливый ... Декоратор впрыскивает Нарисуйте функция в оформленном определении функции. Нарисуйте Функция может быть использована для образца значений от другого генератора. Выше мы обращаемся к списку кортежей ключей из uvery_key_value_tupes () Генератор, а затем вставьте эти пары клавиш в нашу Сортируется Отказ Сейчас uvere_sorted_dicts () это генератор данных, генерирующих экземпляры Сортируется Отказ

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

# test_sorted_dict.py
@some.composite
def some_sorted_dicts(draw):
    """
    Generator of sorted dicts. Returns a tuple of the sorted dictionary and a dictionary holding the key-value pairs used for data generation.
    """
    ... # define sorted_dict as as above

    expected = {}
    for key, val in key_values:
        expected[key] = val

    return sorted_dict, expected

Переменная Ожидается Содержит пары клавишных значений, которые мы ожидаем найти из нашего сортировочного словаря. Пары клавишных значений содержат действие в качестве «модели» для того, что наш сортированный словарь должен содержать. Используя стандартный словарь, поскольку модель полезна, потому что она обрабатывает дубликаты таким же образом, мы ожидаем Сортируется Чтобы обработать, то есть, перезаписи.

Имущество

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

# test_sorted_dict.py
from hypothesis import given

@given(dict_and_values=some_sorted_dicts())
def test_insert_and_search(dict_and_values):
    """
    Key-value pairs added to sorted dictionary can be searched.
    """
    sorted_dict, expected = dict_and_values

    for key, value in expected.items():
        in_dict = sorted_dict[key]
        assert in_dict == value

Тестовый случай помечен как тест гипотезы с @viven Функциональный декоратор. Когда этот тест работает, гипотеза будет генерировать 100 различных сортировков и проверить, что все пары клавиш, ожидаемые в словаре.

Примечание. 100 – это число удовлетворяющих задач для выполнения тестовых случаев, которые будут работать до окончания гипотезы. Вы можете изменить это значение, добавив @settings объект.

Если мы запустим тесты сейчас, они должны потерпеть неудачу: мы не реализовали __setitem__ и __getitem__ пока что. Для тех, нам нужно двоичное поиск.

Двоичное дерево поиска

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

Дерево определяется как Dataclass следующее:

# tree.py
from dataclasses import dataclass
import typing as t

@dataclass
class Tree:
    """
    Binary search tree.
    """

    root: t.Optional[Node] = None

    def __repr__(self):
        return "Tree(root={})".format(repr(self.root))

@dataclass Декоратор добавляет, например, __init__ Способ создания дерева с необязательным root аргумент Если дерево пусто, root равно Нет Отказ В противном случае он содержит Узел Определяется так:

# tree.py

@dataclass
class Node:
    key: int
    value: t.Any
    parent: t.Optional["Node"] = None
    left: t.Optional["Node"] = None
    right: t.Optional["Node"] = None

    def __repr__(self):
        return "Node(key={}, value={}, left=({}), right=({})".format(
            self.key, repr(self.value), repr(self.left), repr(self.right)
        )

A Узел имеет ключ и значение. Он также содержит ссылки на его левый и правильно дети, а также его родитель Отказ

Примечание: родитель не включен в __repr__ Чтобы избежать бесконечных петель (затем печатает ребенка, затем печатает родитель, который печатает ребенка, который печатает родитель …).

Вставка клавиши и значение для дерева происходит следующим образом:

# tree.py
def insert(tree: Tree, key, value):
    """
    Reference insertion algorithm from Introduction to Algorithms.
    Operates in-place.
    """
    y = None
    x = tree.root

    while x is not None:
        y = x
        if key < x.key:
            x = x.left
        elif key > x.key:
            x = x.right
        else:
            x.value = value
            return

    z = Node(key=key, value=value, parent=y)
    if y is None:
        tree.root = z
    elif z.key < y.key:
        y.left = z
    else:
        y.right = z

Примечание: если ключ равно существующему ключу, значение в соответствующем узле обновляется на месте.

Чтобы искать с дерева, нам нужно пойти по дереву, пока совпадение не найдено. Или, если совпадение не найдено, поднимите KeyError :

# tree.py
def search(tree: Tree, key):
    node = _search_node(tree, key)
    return node.value


def _search_node(tree: Tree, key) -> Node:
    if tree.root is None:
        raise KeyError("Empty dictionary, can't find key {}".format(key))

    x = tree.root

    while x is not None:
        if key < x.key:
            x = x.left
        elif key > x.key:
            x = x.right
        else:
            return x

    raise KeyError("Key {} not found".format(key))

Наше Поиск включает в себя функцию помощника _search_node. что ищет Узел по ключу. Нам понадобится это при внедрении удаления позже.

Положить все вместе

С помощью дерева поддерживая вставку и поиск, мы можем использовать это дерево в нашем Сортируется :

# sorted_dict.py
from . import tree

class SortedDict:
    def __init__(self):
        self._tree = tree.Tree()

    def __setitem__(self, key, item):
        tree.insert(self._tree, key, item)

    def __getitem__(self, key):
        return tree.search(self._tree, key)

Если мы сейчас запустим тест на основе недвижимости для вставки и поиска, мы должны увидеть его пропуск!

Теперь мы реализовали первое требование с тестом на основе свойств. Давайте посмотрим на наши оставшиеся требования:

  1. Пары клавиш, добавленные в наш сортирный словарь, можно найти.
  2. Добавление ключа, которое уже существует, перезаписывает существующий.
  3. Ключи всегда отсортированы.
  4. Поиск несуществующего ключа повышает KeyError Отказ
  5. Удаление ключа, а затем ищет его поднимает KeyError Отказ

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

Реализация удаления

Требование гласит, что поиск удаленного ключа поднимает KeyError Отказ Но как мы можем положить это в недвижимость? Один из способов использовать наш uvere_sorted_dicts () Генератор из ранее и пусть гипотеза нарисует одну из ключей для удаления. Затем мы удалите этот ключ и убедитесь, что поиск поднимает KeyError Отказ

Чтобы сделать ключ к удалению, мы могли бы использовать Композитный снова, чтобы создать составную стратегию. Эта стратегия даст отсортированный словарь, словарь ожидаемого содержимого и ключ к удалению. Однако в этом случае он имеет больше смысла использовать встроенный данные Стратегия от гипотезы. Эта стратегия приходит с Нарисуйте метод, и мы можем использовать его, чтобы попробовать значение из генератора Во время теста Отказ

Вот как мы можем использовать Данные () Чтобы попробовать ключ к удалению из списка вставленных клавиш:

# test_sorted_dict.py

@given(
    dict_and_values=some_sorted_dicts(),
    data=some.data(),
)
def test_search_after_delete(dict_and_values, data):
    """
    Searching for a key after deleting that key raises a KeyError.
    """
    sorted_dict, expected = dict_and_values
    inserted_keys = list(expected.keys())
    some_key = some.sampled_from(inserted_keys)
    key_to_delete = data.draw(some_key, label="Key to delete")
    del sorted_dict[key_to_delete]
    with pytest.raises(KeyError):  # type: ignore
        sorted_dict[key_to_delete]

Этот тест впервые использует Sampled_from Чтобы создать генератор под названием que_key Отказ При выборе выбора que_key случайным образом выбирает один ключ из списка вставленных клавиш. Затем мы используем data.draw () Чтобы попробовать ключ, который мы хотим удалить. этикетка Аргумент добавляется так, что гипотеза может распечатать более четкое сообщение об ошибке, если он находит неудачный тестовый случай. Как только ключ нарисован, мы удаляем ключ и убедитесь, что поиск его повышает KeyError Отказ

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

# test_sorted_dict.py

@given(
    dict_and_values=some_sorted_dicts().filter(lambda drawn: len(drawn[1]) > 0),
    data=some.data(),
)
def test_search_after_delete(dict_and_values, data):
    ...

Примечание: на протяжении всей остальной части учебника ... представляет собой остальную часть функции или класса, как это было предыдущим письменным.

Фильтр гарантирует, что словарь не пустым.

С недвижимостью готов, теперь мы можем перейти к реализации. Операция удаления в нашем тесте, del sorted_dict [ключ] , переводит на Сортировать_dict .__ Delitem __ (ключ) . Поэтому нам нужно определить __delitem__ в Сортировка :

# sorted_dict.py
class SortedDict:
    ...

    def __delitem__(self, key):
        return tree.delete(self._tree, key)

Опять же, мы делегируем удаление дереву с дерево функция. Потому что удаление несколько сложно, но может быть скопировано из Введение в алгоритмы Книга, мы ссылаемся на дерево .py в сопроводительном хранилище для его реализации.

Занимая удаление, наш список требований теперь выглядит следующим образом:

  1. Пары клавиш, добавленные в наш сортирный словарь, можно найти.
  2. Добавление ключа, которое уже существует, перезаписывает существующий.
  3. Ключи всегда отсортированы.
  4. Поиск несуществующего ключа повышает KeyError Отказ
  5. Удаление ключа, а затем ищет его поднимает KeyError.

В качестве нашего последнего примера мы напишем тест на основе недвижимости для третьего требования: ключи всегда отсортированы. Недвижимость для окончательного требования (поиск несуществующего ключа повышает keyError ) можно найти в test_sorted_dict.py в сопроводительном хранилище.

Обеспечение ключей отсортировано

Нравится «Ключи», как «Ключи всегда отсортированы», является инвариантным: какие операции выполняются, она должна оставаться верной. Предполагая Сортируется имеет Клавиши () Метод, возвращающий список ключей, мы можем написать тест на основе недвижимости, гарантирую, что все экземпляры Сортируется генерируется uvere_sorted_dicts () Сортированные ключи:

# test_sorted_dict.py
@given(dict_and_values=some_sorted_dicts())
def test_keys_sorted(dict_and_values):
    """
    Invariant: Keys in the sorted dictionary are sorted.
    """
    sorted_dict, _ = dict_and_values
    keys = sorted_dict.keys()
    assert keys == sorted(keys)

Реализация для Сортировать_dict.keys () использует ходьба по заказу пройти дерево. Реализация можно найти в Сортировать_dict.py и дерево .py в сопроводительном хранилище.

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

# test_sorted_dict.py

@given(
    dict_and_values=some_sorted_dicts().filter(lambda drawn: len(drawn[1]) > 0),
    data=some.data(),
)
def test_search_after_delete(dict_and_values, data):
    ...
    keys = sorted_dict.keys()
    assert keys == sorted(keys)

Пока это работает, есть что-то неудовлетворительное об этом. Предоставление тестирования на основе недвижимости заключается в том, что мы можем охватывать все виды неожиданных случаев. В test_keys_sorted и test_search_after_delete Мы тяжело кодируем случаи, когда инвариант должен быть проверен. Есть ли что-то, что мы можем сделать, чтобы рандомизировать тестирование инварианта?

Введите Государственное тестирование Отказ С государственными тестами мы определяем операции, которые могут быть запущены, но каркас решит порядок.

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

# test_sorted_dict.py
class StatefulDictStateMachine(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        self.sorted_dict = SortedDict()
        self.state = {}

    inserted_keys = Bundle("inserted")
    deleted_keys = Bundle("deleted_keys")

    @rule(target=inserted_keys, key=some.integers(), value=some.text())
    def insert(self, key, value):
        event("Inserting key")
        self.sorted_dict[key] = value
        self.state[key] = value
        return key

    @rule(key=inserted_keys)
    def search(self, key):
        # A key inserted before may have already been
        # deleted if it was a duplicate, so searching it
        # may not succeed. Check the key exists in
        # the model dictionary.
        assume(key in self.state)
        event("Searching existing key")
        assert self.sorted_dict[key] == self.state[key]

    @rule(key=consumes(inserted_keys))
    def delete(self, key):
        assume(key in self.state)
        event("Deleting key")
        del self.sorted_dict[key]
        del self.state[key]

    @rule(key=some.integers())
    def search_non_existing(self, key):
        assume(key not in self.state)
        event("Searching non-existing key")
        with pytest.raises(KeyError):  # type: ignore
            self.sorted_dict[key]

    @invariant()
    def keys_sorted(self):
        keys = self.sorted_dict.keys()
        assert keys == sorted(keys)


TestStatefulDict = StatefulDictStateMachine.TestCase

Методы, определенные в нашем Руководительстатемашин , украшены @rule Определите команды прогоны состояния. Декоратор @invariant гарантирует, что мы проверяем после каждой команды, если ключи отсортированы. Мы также поддерживаем модель нашего текущего ожидаемого государства в Self.State Чтобы узнать, какие ключи наше Сортируется ожидается, что будет содержать.

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

Fall Touch: добавьте доку

Помните, как мы играли с ожидаемыми API нашего Сортируется придумать свойства? Например, мы предположили, что следующее будет держать:

>>> # Insert and search
>>> sorted_dict = SortedDict()
>>> sorted_dict[2] = 'two'
>>> sorted_dict[2]
'two'

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

Эти тесты служат документацией, что означает, что они делают отличные докторы Отказ Мы можем добавить тесты в верхней части нашего Сортировать_dict.py Модуль в DocString:

# sorted_dict.py
"""
Sorted, mutable dictionary
keeping keys in sorted order.

Examples:

>>> # Insert and search
>>> sorted_dict = SortedDict()
>>> sorted_dict[2] = 'two'
>>> sorted_dict[2]
'two'
>>> # Handles duplicate keys
>>> sorted_dict[2] = 'two-two'
>>> sorted_dict[2]
'two-two'
...
"""

В pteest мы можем включить докторы с --Доктесты-модули флаг. Добавьте следующее в вашем Pytest.ini Чтобы всегда запустить докторы:

# pytest.ini
[pytest]
addopts = --doctest-modules
# Ignore extraneous whitespaces and exception details.
doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL

Когда pteest Запускается, теперь он также запускает примеры из документации. И с этим мы закончим реализацию функций, которые мы отправились для реализации для наших Сортируется Действительно

Вывод

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

Важно помнить, что тестирование на основе недвижимости не заменяет обычные модульные тесты Отказ Вместо этого он предоставляет новый инструмент для наших тестируемых инструментов. И иногда тестирование на основе недвижимости не правильный инструмент для работы. Для интересной статьи о том, когда тестирование на основе недвижимости сияет, посмотрите на Эта статья Brujo Benavides Отказ Тем не менее, тем более опытным мы получаем на написании тестовых генераторов и тестов на основе собственности, тем более естественным становится использовать их для тестирования практически любого типа кода.

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

Дополнительные ресурсы

Оригинал: “https://dev.to/meeshkan/introduction-to-properties-driven-development-547g”