Что такое property-based тестирование (Hypothesis)?python-61

Property-based тестирование — это подход к тестированию, при котором проверяются не конкретные примеры, а общие свойства (инварианты) кода. В Python для этого используется библиотека Hypothesis.

Основные концепции

  1. Свойство (Property) — утверждение, которое должно быть верным для всех допустимых входных данных
  2. Генерация данных (Data generation) — автоматическое создание тестовых данных
  3. Уменьшение (Shrinking) — автоматическое упрощение провальных тест-кейсов
  4. Инвариант (Invariant) — условие, которое должно сохраняться

Установка

pip install hypothesis

Базовый пример

Проверим свойство коммутативности сложения:

from hypothesis import given
import hypothesis.strategies as st

@given(st.integers(), st.integers())
def test_add_commutative(a, b):
    assert a + b == b + a  # Коммутативность сложения

Стратегии генерации данных

Hypothesis предоставляет множество стратегий:

import hypothesis.strategies as st

# Целые числа
st.integers(min_value=1, max_value=100)

# Строки
st.text(alphabet='abc', min_size=1, max_size=5)

# Списки
st.lists(st.integers(), min_size=1)

# Словари
st.dictionaries(st.text(), st.integers())

# Кастомные типы
st.builds(Person, name=st.text(), age=st.integers())

Пример тестирования функции сортировки

@given(st.lists(st.integers()))
def test_sort_properties(lst):
    sorted_lst = sorted(lst)
    # Проверяем длину
    assert len(sorted_lst) == len(lst)
    # Проверяем порядок
    assert all(sorted_lst[i] <= sorted_lst[i+1] for i in range(len(sorted_lst)-1))
    # Проверяем мультимножество
    assert collections.Counter(sorted_lst) == collections.Counter(lst)

Уменьшение провальных примеров

Когда тест падает, Hypothesis пытается найти минимальный пример:

  1. Находит случай, когда тест падает
  2. Постепенно упрощает входные данные
  3. Показывает минимальный провальный пример

Проверка инвариантов

Пример для банковского счета:

@given(
    st.integers(min_value=0, max_value=1000),
    st.integers(min_value=0, max_value=1000)
)
def test_account_invariants(deposit, withdraw):
    account = Account(balance=1000)
    account.deposit(deposit)
    try:
        account.withdraw(withdraw)
    except InsufficientFunds:
        pass

    assert account.balance >= 0  # Инвариант: баланс не может быть отрицательным

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

@st.composite
def user_strategy(draw):
    name = draw(st.text(min_size=1))
    age = draw(st.integers(min_value=18, max_value=120))
    return {"name": name, "age": age}

@given(user_strategy())
def test_user_validation(user):
    assert validate_user(user["name"], user["age"])

Продвинутые возможности

1. Stateful тестирование

Проверка инвариантов между последовательностью действий:

from hypothesis.stateful import RuleBasedStateMachine, rule

class AccountTest(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        self.account = Account(balance=0)

    @rule(amount=st.integers(min_value=1))
    def deposit(self, amount):
        self.account.deposit(amount)
        assert self.account.balance >= 0

    @rule(amount=st.integers(min_value=1))
    def withdraw(self, amount):
        try:
            self.account.withdraw(amount)
        except InsufficientFunds:
            pass
        assert self.account.balance >= 0

TestAccount = AccountTest.TestCase

2. Настройка параметров

from hypothesis import settings, HealthCheck

@settings(
    max_examples=500,
    suppress_health_check=[HealthCheck.too_slow]
)
@given(st.integers())
def test_large_range(x):
    assert x * 0 == 0

3. Примеры из данных

from hypothesis import example

@given(st.text())
@example("")
@example("a")
@example("test@example.com")
def test_email_validation(email):
    result = validate_email(email)
    assert result == ("@" in email)

Преимущества

  1. Находит неочевидные баги — проверяет краевые случаи автоматически
  2. Экономит время — не нужно придумывать тест-кейсы вручную
  3. Документирует свойства — делает ожидания от кода явными
  4. Улучшает дизайн — заставляет думать об инвариантах

Ограничения

  1. Сложность отладки — провальные примеры могут быть неочевидными
  2. Производительность — может работать медленнее unit-тестов
  3. Не заменяет unit-тесты — дополняет, но не заменяет их полностью

Практические рекомендации

  1. Начинайте с простых свойств
  2. Комбинируйте с примерными тестами
  3. Используйте shrinking для анализа ошибок
  4. Тестируйте не только happy path
  5. Проверяйте инварианты, а не конкретные значения

Резюмируем

  1. Hypothesis — мощная библиотека для property-based тестирования в Python
  2. Генерирует данные автоматически, проверяя общие свойства кода
  3. Полезен для поиска краевых случаев и проверки инвариантов
  4. Комбинируется с традиционными unit-тестами
  5. Требует практики — начинайте с простых примеров

Лучшие кандидаты для property-based тестирования:

  • Математические операции
  • Функции сортировки/фильтрации
  • Сериализация/десериализация
  • Валидация данных
  • Состоятельные системы (stateful systems)