Python · Тестування з pytest · Просунутий
Property-Based тестування з Hypothesis
Автоматично генеруйте тисячі вхідних даних для тестів, щоб знаходити граничні випадки.
Короткий вступ до теми та пояснення перед вправами (вправи нижче):
Hypothesis на практиці: інваріанти, скорочення та складені стратегії
#Довідникова картка: Hypothesis
#Вправи:
Тест: abs(n) >= 0 для всіх цілих чисел
#Встановіть `hypothesis` і напишіть тест властивостей `test_abs_non_negative`, що: - Використовує `@given(st.integers())` для генерації будь-якого цілого числа - Перевіряє, що `abs(n) >= 0` Також напишіть `test_abs_of_abs_equals_abs`, що перевіряє `abs(abs(n)) == abs(n)` для будь-якого цілого числа. Обидва тести мають пройти -- вони тестують правильний вбудований Python.
# test_abs_property.py
from hypothesis import given
from hypothesis import strategies as st
@given(st.integers())
def test_abs_non_negative(n):
...
@given(st.integers())
def test_abs_of_abs_equals_abs(n):
...
Рішення
# test_abs_property.py
from hypothesis import given
from hypothesis import strategies as st
@given(st.integers())
def test_abs_non_negative(n):
assert abs(n) >= 0
@given(st.integers())
def test_abs_of_abs_equals_abs(n):
assert abs(abs(n)) == abs(n)
Тест: реверс рядка двічі повертає оригінал
#Визначте функцію `reverse(s: str) -> str`, що реверсує рядок за допомогою зрізу (`s[::-1]`). Напишіть тест властивостей `test_reverse_involution` з `@given(st.text())`, що перевіряє: ``` reverse(reverse(s)) == s ``` Також напишіть `test_reverse_empty` з `@given(st.text(max_size=0))` -- реверс порожнього рядка є порожнім рядком.
# test_reverse.py
from hypothesis import given
from hypothesis import strategies as st
def reverse(s: str) -> str:
...
@given(st.text())
def test_reverse_involution(s):
...
@given(st.text(max_size=0))
def test_reverse_empty(s):
...
Рішення
# test_reverse.py
from hypothesis import given
from hypothesis import strategies as st
def reverse(s: str) -> str:
return s[::-1]
@given(st.text())
def test_reverse_involution(s):
assert reverse(reverse(s)) == s
@given(st.text(max_size=0))
def test_reverse_empty(s):
assert reverse(s) == ''
Використання assume() для тестування ділення з ненульовим знаменником
#Напишіть функцію `safe_divide(a: float, b: float) -> float`, що повертає `a / b`. Напишіть два тести властивостей: 1. `test_divide_with_assume` -- використайте `@given(st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False), st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False))` і `assume(b != 0)`. Перевірте `safe_divide(a, b) * b == pytest.approx(a)` (множення результату на знаменник дає початковий чисельник). 2. `test_divide_positive_constrained` -- використайте `@given(st.floats(min_value=0.1, max_value=100), st.floats(min_value=0.1, max_value=100))`. Перевірте, що результат є додатнім.
# test_division_property.py
import pytest
from hypothesis import given, assume
from hypothesis import strategies as st
def safe_divide(a: float, b: float) -> float:
return a / b
@given(
st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False),
st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False),
)
def test_divide_with_assume(a, b):
assume(b != 0)
result = safe_divide(a, b)
assert result * b == pytest.approx(a, rel=1e-6)
@given(
st.floats(min_value=0.1, max_value=100),
st.floats(min_value=0.1, max_value=100),
)
def test_divide_positive_constrained(a, b):
result = safe_divide(a, b)
assert result > 0
Рішення
# test_division_property.py
import pytest
from hypothesis import given, assume
from hypothesis import strategies as st
def safe_divide(a: float, b: float) -> float:
return a / b
@given(
st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False),
st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False),
)
def test_divide_with_assume(a, b):
assume(b != 0)
result = safe_divide(a, b)
assert result * b == pytest.approx(a, rel=1e-6)
@given(
st.floats(min_value=0.1, max_value=100),
st.floats(min_value=0.1, max_value=100),
)
def test_divide_positive_constrained(a, b):
result = safe_divide(a, b)
assert result > 0
Тест: max(lst) >= min(lst) для будь-якого непорожнього списку
#Напишіть тест властивостей з `@given(st.lists(st.integers(), min_size=1))`, що перевіряє: - `max(lst) >= min(lst)` - `max(lst)` є елементом `lst` (тобто `max(lst) in lst`) - `min(lst)` є елементом `lst` (тобто `min(lst) in lst`) Всі три властивості мають бути окремими операторами `assert` в одній тест-функції `test_max_min_properties`.
# test_max_min.py
from hypothesis import given
from hypothesis import strategies as st
@given(st.lists(st.integers(), min_size=1))
def test_max_min_properties(lst):
assert ...
assert ...
assert ...
Рішення
# test_max_min.py
from hypothesis import given
from hypothesis import strategies as st
@given(st.lists(st.integers(), min_size=1))
def test_max_min_properties(lst):
assert max(lst) >= min(lst)
assert max(lst) in lst
assert min(lst) in lst
Написання @st.composite стратегії для словника користувача
#Напишіть стратегію `@st.composite` `valid_user(draw)`, що генерує словник користувача з: - `username` -- 3-20 символів з `string.ascii_lowercase + string.digits + '_'` - `age` -- ціле число від 13 до 120 - `email` -- простий фейковий email, побудований як `f'{username}@example.com'` Примітка: виведіть `email` з того самого значення `username`, що ви вибрали (щоб вони були узгодженими). Напишіть функцію `validate_user(user)`, що: - Підіймає `ValueError('Username too short')`, якщо `len(username) < 3` - Підіймає `ValueError('Too young')`, якщо `age < 13` - Повертає `True` в іншому випадку Напишіть `test_valid_user_always_validates` з `@given(valid_user())`, що перевіряє `validate_user(user) is True`.
# test_composite.py
import string
from hypothesis import given
from hypothesis import strategies as st
@st.composite
def valid_user(draw):
username = draw(st.text(
alphabet=...,
min_size=3,
max_size=20,
))
age = draw(st.integers(min_value=13, max_value=120))
return {
'username': username,
'age': age,
'email': f'{username}@example.com',
}
def validate_user(user):
if len(user['username']) < 3:
raise ValueError('Username too short')
if user['age'] < 13:
raise ValueError('Too young')
return True
@given(valid_user())
def test_valid_user_always_validates(user):
assert validate_user(user) is True
Рішення
# test_composite.py
import string
from hypothesis import given
from hypothesis import strategies as st
@st.composite
def valid_user(draw):
username = draw(st.text(
alphabet=string.ascii_lowercase + string.digits + '_',
min_size=3,
max_size=20,
))
age = draw(st.integers(min_value=13, max_value=120))
return {
'username': username,
'age': age,
'email': f'{username}@example.com',
}
def validate_user(user):
if len(user['username']) < 3:
raise ValueError('Username too short')
if user['age'] < 13:
raise ValueError('Too young')
return True
@given(valid_user())
def test_valid_user_always_validates(user):
assert validate_user(user) is True