Архив рубрики ~Лента новостей~

Позвольте гипотезе сломать ваш код Python прежде, чем это сделают ваши пользователи

Позвольте гипотезе сломать ваш код Python прежде, чем это сделают ваши пользователи

Тесты на основе свойств, которые выявляют ошибки, о существовании которых вы не знали.

Делиться

a7c393c67b6f4da28614443b36ca0627

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

«Учел ли я все крайние случаи?»

Вы можете проверять входные данные положительными и отрицательными числами, нулем и пустыми строками. Но как насчёт странных символов Unicode? Или чисел с плавающей точкой, которые равны NaN или бесконечности? А как насчёт списка списков пустых строк или сложного вложенного JSON? Пространство возможных входных данных огромно, и сложно представить себе множество различных причин, по которым ваш код может сломаться, особенно если вы ограничены во времени.

Тестирование на основе свойств перекладывает эту нагрузку с вас на инструменты. Вместо того, чтобы вручную выбирать примеры, вы формулируете свойство — истину, которая должна выполняться для всех входных данных. Затем библиотека Hypothesis генерирует входные данные (при необходимости несколько сотен), ищет контрпримеры и, если находит, сжимает их до простейшего ложного случая.

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

Так что же такое тестирование на основе свойств?

Тестирование на основе свойств — это методология, в которой вместо написания тестов для конкретных, жёстко заданных примеров вы определяете общие «свойства» или «инварианты» кода. Свойство — это высокоуровневое утверждение о поведении кода, которое должно выполняться для всех допустимых входных данных. Затем вы используете фреймворк тестирования, например, Hypothesis, который интеллектуально генерирует широкий диапазон входных данных и пытается найти «контрпример» — конкретный входной параметр, для которого указанное вами свойство ложно.

Некоторые ключевые аспекты тестирования на основе свойств с помощью Hypothesis включают в себя:

  • Генеративное тестирование. Hypothesis генерирует для вас тестовые случаи — от простых до необычных, исследуя пограничные случаи, которые вы, вероятно, могли бы пропустить.
  • На основе свойств. Это меняет ваше мышление с вопроса «каков выходной результат для этих конкретных входных данных?» на вопрос «каковы универсальные истины о поведении моей функции?»
  • Сжатие. Это убийственная функция Hypothesis. Когда система обнаруживает неудавшийся тестовый случай (который может быть большим и сложным), она не просто сообщает об этом. Она автоматически «сжимает» входные данные до минимально возможного и простого примера, который всё ещё вызывает сбой, что часто значительно упрощает отладку.
  • Тестирование с отслеживанием состояния. Гипотезы позволяют тестировать не только чистые функции, но и взаимодействия и изменения состояний сложных объектов посредством последовательности вызовов методов.
  • Расширяемые стратегии. Hypothesis предоставляет надежную библиотеку «стратегий» для генерации данных и позволяет вам составлять их или создавать совершенно новые в соответствии с моделями данных вашего приложения.

Почему гипотезы важны / Распространенные случаи использования

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

Гипотеза особенно эффективна для проверки:

  • Сериализация/десериализация. Классическое свойство заключается в том, что для любого объекта x функция decode(encode(x)) должна быть равна x. Это идеально подходит для тестирования функций, работающих с JSON или пользовательскими двоичными форматами.
  • Сложная бизнес-логика. Любая функция со сложной условной логикой — отличный кандидат. Гипотеза исследует пути в вашем коде, которые вы, возможно, не рассматривали.
  • Системы с отслеживанием состояния. Тестирование классов и объектов для гарантии того, что никакая последовательность допустимых операций не может привести объект в повреждённое или недопустимое состояние.
  • Тестирование на основе эталонной реализации. Вы можете сформулировать свойство, согласно которому ваша новая оптимизированная функция всегда должна давать тот же результат, что и более простая, известная, образцовая эталонная реализация.
  • Функции, принимающие сложные модели данных. Тестирование функций, принимающих в качестве входных данных модели Pydantic, классы данных или другие пользовательские объекты.

Настройка среды разработки

Всё, что вам нужно, — это Python и pip. Мы установим pytest в качестве инструмента для запуска тестов, саму гипотезу и pydantic для одного из наших продвинутых примеров.

(база) tom@tpr-desktop:~$ python -m venv hyp-env (база) tom@tpr-desktop:~$ source hyp-env/bin/activate (hyp-env) (база) tom@tpr-desktop:~$ # Установка pytest, hypothesis и pydantic (hyp-env) (база) tom@tpr-desktop:~$ pip install pytest hypothesis pydantic # создание новой папки для хранения кода Python (hyp-env) (база) tom@tpr-desktop:~$ mkdir hyp-project

Гипотезу лучше всего проверять с помощью проверенного инструмента для запуска тестов, например pytest, так мы и сделаем.

Пример кода 1 — Простой тест

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

Проверка гипотез определяется с помощью двух элементов: декоратора @given и стратегии , которая передаётся декоратору. Представьте стратегию как типы данных, которые Hypothesis сгенерирует для проверки вашей функции. Вот простой пример. Сначала мы определяем функцию, которую хотим протестировать.

# my_geometry.py def calculate_rectangle_area(length: int, width: int) -> int: «»» Вычисляет площадь прямоугольника по его длине и ширине. Эта функция возвращает ValueError, если хотя бы одно из измерений не является положительным целым числом. «»» если не isinstance(length, int) или не isinstance(width, int): raise TypeError(«Длина и ширина должны быть целыми числами.») если длина <= 0 или ширина <= 0: raise ValueError("Длина и ширина должны быть положительными.") return длина * ширина

Далее следует функция тестирования.

# test_rectangle.py from my_geometry import calculate_rectangle_area from hypothesis import given, strategies as st import pytest # Используя st.integers(min_value=1) для обоих аргументов, мы гарантируем, # что Hypothesis сгенерирует только допустимые входные данные для нашей функции. @given( length=st.integers(min_value=1), width=st.integers(min_value=1) ) def test_rectangle_area_with_valid_inputs(length, width): «»» Свойство: Для любых положительных целых чисел length и width площадь должна быть равна их произведению. Этот тест гарантирует правильность базовой логики умножения. «»» print(f»Testing with valid inputs: length={length}, width={width}») # Проверяемое нами свойство — это математическое определение площади. assert calculate_rectangle_area(length, width) == length * width

Добавление декоратора @given к функции превращает её в проверку гипотезы. Передача стратегии (st.integers) декоратору означает, что гипотеза должна генерировать случайные целые числа для аргумента n при проверке, но мы накладываем дополнительные ограничения, гарантируя, что ни одно целое число не может быть меньше единицы.

Мы можем запустить этот тест, вызвав его следующим образом.

(hyp-env) (база) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_my_geometry.py == … длина=6541, ширина=1 Тестирование с допустимыми входными данными: длина=6541, ширина=28545 Тестирование с допустимыми входными данными: длина=1295885530, ширина=1 Тестирование с допустимыми входными данными: длина=1295885530, ширина=25191 Тестирование с допустимыми входными данными: длина=14538, ширина=1 Тестирование с допустимыми входными данными: длина=14538, ширина=15503 Тестирование с допустимыми входными данными: длина=7997, ширина=1 … … Тестирование с допустимыми входными данными: длина=19378, ширина=22512 Тестирование с допустимыми входными данными: длина=22512, ширина=22512 Тестирование с допустимыми входными данными: длина=3392, ширина=44 Тестирование с допустимыми входными данными: длина=44, ширина=44 . ================================================ 1 пройдено за 0,10 с ==================================================

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

из гипотезы импортируется заданное, стратегии как st,settings … … @given( длина=st.integers(min_value=1), ширина=st.integers(min_value=1) ) @settings(max_examples=3) def test_rectangle_area_with_valid_inputs(длина, ширина): … … # # Выходы # (hyp-env) (база) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_my_geometry.py ================================================= запуск тестовой сессии ===================================================== платформа linux — Python 3.11.10, pytest-8.4.0, pluggy-1.6.0 корневой каталог: /home/tom/hypothesis_project плагины: hypothesis-6.135.9, anyio-4.9.0 собрано 1 элемент test_my_geometry.py Тестирование с допустимыми входными данными: длина = 1, ширина = 1 Тестирование с допустимыми входными данными: длина = 1870, ширина = 5773964720159522347 Тестирование с допустимыми входными данными: длина = 61, ширина = 25429 . =================================================== 1 пройдено за 0,06 с ===================================================

Пример кода 2 — Тестирование классического свойства «кругового обхода»

Давайте рассмотрим классическое свойство: сериализация и десериализация должны быть обратимы. Короче говоря, decode(encode(X)) должен возвращать X.

Мы напишем функцию, которая берет словарь и кодирует его в строку URL-запроса.

Создайте в папке вашего hyp-проекта файл с именем my_encoders.py.

# my_encoders.py import urllib.parse def encode_dict_to_querystring(data: dict) -> str: # Здесь есть ошибка: он плохо обрабатывает вложенные структуры return urllib.parse.urlencode(data) def decode_querystring_to_dict(qs: str) -> dict: return dict(urllib.parse.parse_qsl(qs))

Это две элементарные функции. Что с ними может пойти не так? Теперь давайте проверим их в test_encoders.py:
# test_encoders.py

# test_encoders.py из гипотезы import given, strategies as st # Стратегия для генерации словарей с простыми текстовыми ключами и значениями simple_dict_strategy = st.dictionaries(keys=st.text(), values=st.text()) @given(data=simple_dict_strategy) def test_querystring_roundtrip(data): «»»Свойство: декодирование закодированного словаря должно дать исходный словарь.»»» encoded = encode_dict_to_querystring(data) decoded = decode_querystring_to_dict(encoded) # Мы должны быть осторожны с типами: parse_qsl возвращает строковые значения # Поэтому мы преобразуем наши исходные значения в строки для честного сравнения original_as_str = {k: str(v) for k, v in data.items()} assert decoded == original_as_st

Теперь мы можем запустить наш тест.

(hyp-env) (база) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_encoders.py == … == … _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ data = {'': {}} @given(data=st.recursive( # Базовый случай: Плоский словарь текстовых ключей и простых значений (текст или целые числа). st.dictionaries(st.text(), st.integers() | st.text()), # Рекурсивный шаг: Разрешить значениям быть самими словарями. lambda children: st.dictionaries(st.text(), children) )) def test_for_nesting_limitation(data): «»» Этот тест утверждает, что декодированная структура данных соответствует исходной. Он завершится ошибкой, поскольку urlencode сглаживает вложенные структуры. «»» encoded = encode_dict_to_querystring(data) decoded = decode_querystring_to_dict(encoded) # Это намеренно упрощённое утверждение. Оно не сработает для вложенных # словарей, потому что версия `decoded` будет иметь # строковый внутренний словарь, в то время как версия `data` будет иметь настоящий внутренний словарь. # Так мы обнаруживаем ошибку. > assert decoded == data E AssertionError: assert {'': '{}'} == {'': {}} EE Различающиеся элементы: E {'': '{}'} != {'': {}} E Используйте -v для получения дополнительных различий E Пример фальсификации: test_for_nesting_limitation( E data={'': {}}, E ) test_encoders.py:24: AssertionError ============================================= краткая сводка теста ===================================================== ПРОВАЛ test_encoders.py::test_for_nesting_limitation — AssertionError: assert {'': '{}'} == {'': {}}

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

  • Пример фальсификации. Самая важная подсказка находится в самом низу. Гипотеза сообщает нам точные входные данные, которые взламывают код.

test_for_nesting_limitation(данные={'': {}}, )

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

AssertionError: assert {'': '{}'} == {'': {}}

В этом и заключается суть проблемы. Исходные данные, использованные для теста, были {'': {}}. Декодированный результат, полученный вашими функциями, был {'': '{}'}. Это показывает, что для ключа '' значения различаются:

  • В декодированном виде значение представляет собой строку '{}'.
  • В данных значением является словарь {}.

Строка не равна словарю, поэтому утверждение assert decoded == data равно False , и тест не пройден.

Пошаговое отслеживание ошибки

Наша функция encode_dict_to_querystring использует urllib.parse.urlencode. Когда urlencode встречает значение, являющееся словарём (например, {}), она не знает, как его обработать, поэтому просто преобразует его в строковое представление ('{}').

Информация об исходном типе значения (о том, что это был словарь) теряется навсегда .

Когда функция decode_querystring_to_dict считывает данные обратно, она корректно декодирует значение как строку '{}'. Она не может знать, что изначально это был словарь.

Решение: кодировать вложенные значения как строки JSON

Решение простое,

  1. Кодирование. Перед URL-кодированием проверьте каждое значение в словаре. Если значение — это словарь или список, сначала преобразуйте его в строку JSON.
  2. Декодируйте. После декодирования URL проверьте каждое значение. Если значение похоже на строку JSON (например, начинается с { или [), преобразуйте его обратно в объект Python.
  3. Сделать наше тестирование более полным . Наш декоратор стал сложнее. Проще говоря, он предписывает Hypothesis генерировать словари, которые могут содержать другие словари в качестве значений, что позволяет создавать вложенные структуры данных любой глубины. Например,
  • Простой, плоский словарь: {'name': 'Alice', 'city': 'London'}
  • Одноуровневый вложенный словарь: {'user': {'id': '123', 'name': 'Tom'}}
  • Двухуровневый вложенный словарь: {'config': {'database': {'host': 'localhost'}}}
  • И так далее…

Вот исправленный код.

# test_encoders.py from my_encoders import encode_dict_to_querystring, decode_querystring_to_dict from hypothesis import given, strategies as st # ============================================================================= # ТЕСТ 1: Этот тест доказывает правильность логики ВЛОЖЕНИЯ. # Он использует стратегию, которая генерирует ТОЛЬКО строки, поэтому нам не нужно # беспокоиться о преобразовании типов. Этот тест ПРОЙДЁТ. # ========================================================================= @given(data=st.recursive( st.dictionaries(st.text(), st.text()), lambda children: st.dictionaries(st.text(), children) )) def test_roundtrip_preserves_nested_structure(data): «»»Свойство: Цикл кодирования/декодирования должен сохранять вложенные структуры.»»» encoded = encode_dict_to_querystring(data) decoded = decode_querystring_to_dict(encoded) assert decoded == data # ============================================================================== # ТЕСТ 2: Этот тест доказывает корректность логики ПРЕОБРАЗОВАНИЯ ТИПА # для простых словарей FLAT. Этот тест также будет ПРОЙДЕН. # ========================================================================== @given(data=st.dictionaries(st.text(), st.integers() | st.text())) def test_roundtrip_stringifies_simple_values(data): «»» Свойство: Цикл должен преобразовывать простые значения (например, целые числа) в строки. «»» encoded = encode_dict_to_querystring(data) decoded = decode_querystring_to_dict(encoded) # Создаем модель того, что ожидаем: словарь со строковыми значениями. expected_data = {k: str(v) for k, v in data.items()} assert decoded == expected_data

Теперь, если мы снова запустим наш тест, мы получим следующее:

(hyp-env) (база) tom@tpr-desktop:~/hypothesis_project$ pytest == … [100%] =============================================== 1 пройдено за 0,16 с ==================================================

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

Пример кода 3 — Создание собственной стратегии для модели Pydantic

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

Давайте определим модель в my_models.py.

# my_models.py из pydantic import BaseModel, Field из typing import List class Product(BaseModel): id: int = Field(gt=0) name: str = Field(min_length=1) tags: List[str] def calculate_shipping_cost(product: Product, weight_kg: float) -> float: # Ошибочный калькулятор стоимости доставки cost = 10.0 + (weight_kg * 1.5) if «fragile» in product.tags: cost *= 1.5 # Доплата за хрупкие предметы if weight_kg > 10: cost += 20 # Доплата за тяжелые предметы # Ошибка: что делать, если cost отрицательный? return cost

Теперь в test_shipping.py мы создадим стратегию для генерации экземпляров Product и протестируем нашу ошибочную функцию.

# test_shipping.py из my_models import Product, calculate_shipping_cost из hypothesis import given, strategies as st # Построить стратегию для нашей модели Product product_strategy = st.builds( Product, id=st.integers(min_value=1), name=st.text(min_size=1), tags=st.lists(st.sampled_from([«electronics», «books», «fragile», «clothing»])) ) @given( product=product_strategy, weight_kg=st.floats(min_value=-10, max_value=100, allow_nan=False, allow_infinity=False) ) def test_shipping_cost_is_always_positive(product, weight_kg): «»»Свойство: Стоимость доставки никогда не должна быть отрицательной.»»» cost = calculate_shipping_cost(product, weight_kg) assert cost >= 0

А что показал тест?

(hyp-env) (база) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_shipping.py == … == … _ … calculate_shipping_cost(product, weight_kg) > assert cost >= 0 E assert -0.5 >= 0 E Пример фальсификации: test_shipping_cost_is_always_positive( E product=Product(id=1, name='0', tags=[]), E weight_kg=-7.0, E ) test_shipping.py:19: AssertionError ============================================================== краткий обзор теста информация ===================================================================== ПРОВАЛ test_shipping.py::test_shipping_cost_is_always_positive — assert -0.5 >= 0 ================================================================= 1 ошибка за 0,12 с ====================================================================

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

Пример кода 4 — Тестирование классов с сохранением состояния

Hypothesis может делать больше, чем просто тестировать чистые функции. Он может тестировать классы с внутренним состоянием, генерируя последовательности вызовов методов, чтобы попытаться их разрушить. Давайте протестируем простой пользовательский класс LimitedCache.

my_cache.py

# my_cache.py class LimitedCache: def __init__(self, capacity: int): if capacity <= 0: raise ValueError("Capacity must be positive") self._cache = {} self._capacity = capacity # Ошибка: для правильного LRU это, вероятно, должна быть deque или упорядоченный словарь self._keys_in_order = [] def put(self, key, value): if key not in self._cache and len(self._cache) >= self._capacity: # Вытесняем самый старый элемент key_to_evict = self._keys_in_order.pop(0) del self._cache[key_to_evict] if key not in self._keys_in_order: self._keys_in_order.append(key) self._cache[key] = value def get(self, key): return self._cache.get(key) @property def размер(self): вернуть len(self._cache)

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

Создайте файл test_cache.py.

from hypothesis import strategies as st from hypothesis.stateful import RuleBasedStateMachine, rule, precondition from my_cache import LimitedCache class CacheMachine(RuleBasedStateMachine): def __init__(self): super().__init__() self.cache = LimitedCache(capacity=3) # Это правило добавляет 3 начальных элемента для заполнения кэша @rule( k1=st.just('a'), k2=st.just('b'), k3=st.just('c'), v1=st.integers(), v2=st.integers(), v3=st.integers() ) def fill_cache(self, k1, v1, k2, v2, k3, v3): self.cache.put(k1, v1) self.cache.put(k2, v2) self.cache.put(k3, v3) # Это правило может выполняться только ПОСЛЕ заполнения кэша. # Он проверяет основную логику LRU и FIFO. @precondition(lambda self: self.cache.size == 3) @rule() def test_update_behavior(self): «»» Свойство: Обновление самого старого элемента ('a') должно сделать его самым новым, поэтому следующее вытеснение должно удалить второй по возрасту элемент ('b'). Наш глючный кэш FIFO в любом случае неправильно удалит 'a'. «»» # На этом этапе keys_in_order равен ['a', 'b', 'c']. # 'a' — самый старый. # Мы «используем» 'a' снова, обновив его. В правильном кэше LRU # это сделало бы 'a' последним использованным элементом. self.cache.put('a', 999) # Теперь мы добавляем новый ключ, что должно вызвать вытеснение. self.cache.put('d', 4) # Корректный кэш LRU вытеснит 'b'. # Наш ошибочный кэш FIFO вытеснит 'a'. # Это утверждение проверяет состояние 'a'. # В нашем ошибочном кэше get('a') будет None, поэтому это завершится ошибкой. assert self.cache.get('a') is not None, «Элемент 'a' был неправильно вытеснен» # Это сообщает pytest о необходимости запустить тест конечного автомата TestCache = CacheMachine.TestCase

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

(hyp-env) (база) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_cache.py == … == … ../hyp-env/lib/python3.11/site-packages/hypothesis/stateful.py:476: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ../hyp-env/lib/python3.11/site-packages/hypothesis/stateful.py:258: в run_state_machine_as_test state_machine_test(state_machine_factory) ../hyp-env/lib/python3.11/site-packages/hypothesis/stateful.py:115: в run_state_machine @given(st.data()) ^^^^^^^^ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = CacheMachine({}) @precondition(lambda self: self.cache.size == 3) @rule() def test_update_behavior(self): «»» Свойство: Обновление самого старого элемента ('a') должно сделать его самым новым, поэтому следующее вытеснение должно удалить второй по возрасту элемент ('b'). Наш ошибочный кэш FIFO в любом случае неправильно удалит 'a'. «»» # На данный момент keys_in_order равен ['a', 'b', 'c']. # 'a' — самый старый. # Мы снова «используем» 'a', обновив его. В правильном кэше LRU # это сделало бы 'a' последним использованным элементом. self.cache.put('a', 999) # Теперь мы добавляем новый ключ, что должно вызвать вытеснение. self.cache.put('d', 4) # Корректный кэш LRU вытеснил бы 'b'. # Наш ошибочный кэш FIFO вытеснит 'a'. # Это утверждение проверяет состояние 'a'. # В нашем ошибочном кэше get('a') будет None, поэтому это не сработает. > assert self.cache.get('a') is not None, «Элемент 'a' был неправильно вытеснен» E AssertionError: Элемент 'a' был неправильно вытеснен E assert None is not None E + where None = get('a') E + where get = .get E + where = CacheMachine({}).cache E Пример фальсификации: E state = CacheMachine() E state.fill_cache(k1='a', k2='b', k3='c', v1=0, v2=0, v3=0) E state.test_update_behavior() E state.teardown() test_cache.py:44: AssertionError == … ===========================================================

Вывод выше выявляет ошибку в коде. Проще говоря, этот вывод показывает, что кэш Не является полноценным кэшем «наименее давно использованных» (LRU) . У него есть следующий существенный недостаток:

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

Пример кода 5 — Тестирование с использованием более простой эталонной реализации

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

Предположим, у нас есть простая функция sum_list_simple и новая, «оптимизированная» sum_list_fast, в которой есть ошибка.

my_sums.py

# my_sums.py def sum_list_simple(data: list[int]) -> int: # Это наша простая и правильная эталонная реализация return sum(data) def sum_list_fast(data: list[int]) -> int: # Новая «быстрая» реализация с ошибкой (например, переполнением целочисленных значений для больших чисел) # или, в данном случае, простой ошибкой. total = 0 for x in data: # Ошибка: это должно быть += total = x return total

test_sums.py

# test_sums.py from my_sums import sum_list_simple, sum_list_fast from hypothesis import given, strategies as st @given(st.lists(st.integers())) def test_fast_sum_matches_simple_sum(data): «»» Свойство: Результат новой, быстрой функции всегда должен совпадать с результатом простой, ссылочной функции. «»» assert sum_list_fast(data) == sum_list_simple(data)

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

(hyp-env) (база) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_my_sums.py == … == … _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ data = [1, 0] @given(st.lists(st.integers())) def test_fast_sum_matches_simple_sum(data): «»» Свойство: Результат новой, быстрой функции всегда должен совпадать с результатом простой, ссылочной функции. «»» > assert sum_list_fast(data) == sum_list_simple(data) E assert 0 == 1 E + где 0 = sum_list_fast([1, 0]) E + и 1 = sum_list_simple([1, 0]) E Пример фальсификации: test_fast_sum_matches_simple_sum( E data=[1, 0], E ) test_my_sums.py:11: AssertionError ============================================== краткий обзор теста == …

Итак, тест не пройден, потому что «быстрая» функция суммы дала неправильный ответ (0) для входного списка [1, 0], тогда как правильный ответ, предоставленный «простой» функцией суммы, был 1. Теперь, когда вы знаете, в чем проблема, вы можете предпринять шаги для ее исправления.

Краткое содержание

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

  • Протестируйте свойство «кругового обхода» и посмотрите, как более сложные стратегии обработки данных могут выявить ограничения в нашем коде.
  • Создавайте индивидуальные стратегии для генерации экземпляров сложных моделей Pydantic для тестирования бизнес-логики.
  • Используйте RuleBasedStateMachine для проверки поведения классов с сохранением состояния путем генерации последовательностей вызовов методов.
  • Проверьте сложную оптимизированную функцию, протестировав ее на более простой и заведомо хорошей эталонной реализации.

Добавление тестов на основе свойств в ваш инструментарий не заменит все существующие тесты. Тем не менее, оно значительно их дополнит, заставив вас более чётко продумывать контракты вашего кода и значительно повысив уверенность в его корректности. Я рекомендую вам выбрать функцию или класс в вашей кодовой базе, подумать о его фундаментальных свойствах и позволить Hypothesis постараться доказать вашу неправоту. Благодаря этому вы станете лучшим разработчиком.

Я лишь поверхностно рассказал о возможностях Hypothesis для вашего тестирования. Подробнее см. в официальной документации, доступной по ссылке ниже.

https://hypothesis.readthedocs.io/en/latest

Источник: towardsdatascience.com

Оцените материал:

Читайте также
Архив рубрики ~Обо всем~ Rocket Report: Ракета «Нова» проходит испытательный цикл; IPO SpaceX состоится в пятницу. Архив рубрики ~Обо всем~ Я позволяю Siri видеть мою жизнь на Vision Pro, и это предзнаменование грядущих событий. Архив рубрики ~Обо всем~ Новая электронная книга Boox Go 6 поддерживает использование стилуса для ведения заметок. Новости робототехники Вера в немецких роботов вышла за пределы ЕС Архив рубрики ~Обо всем~ Инженеры, застрявшие внутри, говорят, что созданный всего несколько месяцев назад блок искусственного интеллекта компании Meta — это настоящий ГУЛАГ, где царит атмосфера отчаяния. Архив рубрики ~Коротко из Telegram~ Математики объявили войну ИИ-хайпу Более 150 ведущих мировых математиков опубликовали… Архив рубрики ~Коротко из Telegram~ Siri переезжает в Spotlight и получает доступ к файлам Apple… Архив рубрики ~Коротко из Telegram~ Скилл, который отучает ИИ делать одинаковые сайты Для агентного фронтенда… Архив рубрики ~Коротко из Telegram~ Собираем себе ИИ-офис из агентов Появился Agent Teams — инструмент,… Архив рубрики ~Коротко из Telegram~ Siri стала Siri AI — теперь это не просто ассистент,… Архив рубрики ~Коротко из Telegram~ ХАЛЯВА ОТ NVIDIA: они раздают доступ к 95 бесплатным API… Архив рубрики ~Коротко из Telegram~ Запускаем 500 ИИ-агентов бесплатно — платформа Stack AI раздала щедрый… Архив рубрики ~Коротко из Telegram~ Генерим фотки и видео БЕСПЛАТНО — умельцы выкатили полностью халявный… Архив рубрики ~Обо всем~ Конфиденциальная подача проекта формы S-1 в Комиссию по ценным бумагам и биржам США | OpenAI Архив рубрики ~Обо всем~ Rocket Report: Ракета «Нова» проходит испытательный цикл; IPO SpaceX состоится в пятницу. Архив рубрики ~Обо всем~ Я позволяю Siri видеть мою жизнь на Vision Pro, и это предзнаменование грядущих событий. Архив рубрики ~Обо всем~ Новая электронная книга Boox Go 6 поддерживает использование стилуса для ведения заметок. Новости робототехники Вера в немецких роботов вышла за пределы ЕС Архив рубрики ~Обо всем~ Инженеры, застрявшие внутри, говорят, что созданный всего несколько месяцев назад блок искусственного интеллекта компании Meta — это настоящий ГУЛАГ, где царит атмосфера отчаяния. Архив рубрики ~Коротко из Telegram~ Математики объявили войну ИИ-хайпу Более 150 ведущих мировых математиков опубликовали… Архив рубрики ~Коротко из Telegram~ Siri переезжает в Spotlight и получает доступ к файлам Apple… Архив рубрики ~Коротко из Telegram~ Скилл, который отучает ИИ делать одинаковые сайты Для агентного фронтенда… Архив рубрики ~Коротко из Telegram~ Собираем себе ИИ-офис из агентов Появился Agent Teams — инструмент,… Архив рубрики ~Коротко из Telegram~ Siri стала Siri AI — теперь это не просто ассистент,… Архив рубрики ~Коротко из Telegram~ ХАЛЯВА ОТ NVIDIA: они раздают доступ к 95 бесплатным API… Архив рубрики ~Коротко из Telegram~ Запускаем 500 ИИ-агентов бесплатно — платформа Stack AI раздала щедрый… Архив рубрики ~Коротко из Telegram~ Генерим фотки и видео БЕСПЛАТНО — умельцы выкатили полностью халявный… Архив рубрики ~Обо всем~ Конфиденциальная подача проекта формы S-1 в Комиссию по ценным бумагам и биржам США | OpenAI