Property-based тестирование — это подход к тестированию, при котором проверяются не конкретные примеры, а общие свойства (инварианты) кода. В Python для этого используется библиотека Hypothesis.
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 пытается найти минимальный пример:
Пример для банковского счета:
@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"])
Проверка инвариантов между последовательностью действий:
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
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
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)
Лучшие кандидаты для property-based тестирования: