← Назад к вопросам
При каких условиях должны падать unit-тесты
2.0 Middle🔥 171 комментариев
#Тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Когда unit-тесты должны падать
Unit-тесты должны падать только когда нарушается контракт (договор) функции. Всё остальное — это признак плохо написанного теста.
1. Нарушение контракта функции
Контракт — это то, что функция гарантирует при правильном использовании.
def calculate_discount(age: int, purchase_amount: float) -> float:
"""
Контракт:
- Принимает age >= 0
- Принимает purchase_amount >= 0
- Возвращает скидку (0.0 до 1.0)
- Скидка >= 0.5 если age > 65
"""
if age > 65:
return 0.5
elif age > 18:
return 0.1
else:
return 0.0
# Тесты должны падать когда контракт нарушается:
def test_senior_citizen_discount():
assert calculate_discount(age=70, purchase_amount=100) == 0.5
# ПАДАЕТ если возвращается 0.1 вместо 0.5
# Правильно: контракт нарушен
def test_adult_discount():
assert calculate_discount(age=25, purchase_amount=100) == 0.1
# ПАДАЕТ если возвращается 0.0 вместо 0.1
# Правильно: контракт нарушен
def test_child_no_discount():
assert calculate_discount(age=10, purchase_amount=100) == 0.0
# ПАДАЕТ если возвращается > 0.0
# Правильно: контракт нарушен
2. Регрессия (когда рабочее стало нерабочим)
Тесты должны гарантировать, что функция остаётся стабильной:
def find_user_by_email(email: str, users: list) -> User | None:
"""Найти пользователя по email (case-insensitive)"""
email_lower = email.lower()
for user in users:
if user.email.lower() == email_lower:
return user
return None
def test_case_insensitive_search():
users = [User(email='Alice@Example.com')]
# Должны найти пользователя
assert find_user_by_email('alice@example.com', users) is not None
assert find_user_by_email('ALICE@EXAMPLE.COM', users) is not None
# ПАДАЕТ если разработчик удалит .lower()
# Правильно: регрессия поймана
3. Изменение requirements
Если требование изменилось, нужно обновить тест (и функцию):
# OLD: Скидка 10% для всех adults
def test_old_discount():
assert calculate_discount(age=25) == 0.1
# NEW: Скидка 15% для всех adults (изменилось требование)
def test_new_discount():
assert calculate_discount(age=25) == 0.15
# ПАДАЕТ на старом коде, и это ПРАВИЛЬНО
# Нужно обновить функцию, потом тест зелёный
Когда unit-тесты НЕ должны падать
❌ Зависимость от времени
# ПЛОХО: Тест зависит от текущего времени
def test_is_morning():
current_hour = datetime.now().hour
assert current_hour < 12 # Падает в 14:00!
# Неопределённое поведение
# ХОРОШО: Мокируем время
from unittest.mock import patch
@patch('datetime.datetime')
def test_is_morning(mock_datetime):
mock_datetime.now.return_value = datetime(2024, 1, 15, 10, 30)
assert is_morning() == True
mock_datetime.now.return_value = datetime(2024, 1, 15, 15, 30)
assert is_morning() == False
# Всегда зелёный, независимо от времени суток
❌ Зависимость от порядка выполнения
# ПЛОХО: Один тест зависит от результата другого
shared_state = []
def test_add_user():
shared_state.append({'id': 1, 'name': 'Alice'})
assert len(shared_state) == 1
def test_get_user():
# Может падать если test_add_user не запустился первым
assert shared_state[0]['name'] == 'Alice'
# ХОРОШО: Каждый тест независим
def test_add_user():
state = []
state.append({'id': 1, 'name': 'Alice'})
assert len(state) == 1
def test_get_user():
state = [{'id': 1, 'name': 'Alice'}]
assert state[0]['name'] == 'Alice'
# Всегда работает, независимо от порядка
❌ Зависимость от внешних сервисов
# ПЛОХО: Тест падает если Stripe недоступен
def test_payment_processing():
order = Order(amount=100)
payment = process_payment(order) # Реальный HTTP запрос
assert payment.status == 'success'
# Может падать из-за сетевых ошибок
# ХОРОШО: Мокируем Stripe
from unittest.mock import patch
@patch('stripe.Charge.create')
def test_payment_processing(mock_stripe):
mock_stripe.return_value = {'status': 'succeeded', 'id': 'ch_123'}
order = Order(amount=100)
payment = process_payment(order)
assert payment.status == 'success'
# Всегда зелёный, не зависит от Stripe API
❌ Зависимость от случайности
# ПЛОХО: Может падать непредсказуемо
def test_shuffle():
items = [1, 2, 3]
shuffled = shuffle(items)
assert shuffled == [1, 2, 3] # Часто падает!
# ХОРОШО: Мокируем random
from unittest.mock import patch
@patch('random.shuffle')
def test_shuffle(mock_random):
mock_random.side_effect = lambda x: x.reverse()
items = [1, 2, 3]
shuffled = shuffle(items)
assert shuffled == [3, 2, 1]
# Всегда зелёный, поведение предсказуемо
❌ Зависимость от деталей реализации
# ПЛОХО: Тест знает слишком много внутренних деталей
def test_user_validation():
user = User(name='Alice', age=25)
# Проверяем внутренний атрибут, а не поведение
assert user._cache_timestamp == datetime.now().timestamp()
# Падает если реализация изменилась
# ХОРОШО: Проверяем поведение, не реализацию
def test_user_validation():
user = User(name='Alice', age=25)
# Проверяем что функция работает, не как она устроена
assert user.is_valid() == True
assert user.name == 'Alice'
❌ Тесты с hardcoded magic numbers
# ПЛОХО: Непонятно откуда число
def test_calculate_tax():
assert calculate_tax(100) == 13.0
# Откуда 13? Непонятно
# ХОРОШО: Явные константы или комментарии
TAX_RATE = 0.13
def test_calculate_tax():
amount = 100
expected_tax = amount * TAX_RATE
assert calculate_tax(amount) == expected_tax
# Ясно откуда число
Правило большого пальца (Rule of Thumb)
Тест должен падать тогда и только тогда, когда:
- ✅ Код нарушает свой контракт (interface)
- ✅ Код регрессирует (перестаёт делать что-то, что делал раньше)
- ✅ Требования изменились, и код не соответствует новым требованиям
Тест НЕ должен падать:
- ❌ Из-за внешних факторов (время, сеть, random)
- ❌ Из-за порядка выполнения тестов
- ❌ Из-за деталей реализации (которые могут меняться)
- ❌ Из-за того, что мокировали неправильно
Пример правильного набора тестов
class TestCalculateDiscount:
"""Тесты для функции calculate_discount"""
def test_senior_citizen_gets_max_discount(self):
"""ПАДАЕТ если старики не получают скидку"""
assert calculate_discount(age=70) >= 0.5
def test_adult_gets_standard_discount(self):
"""ПАДАЕТ если adults не получают скидку"""
assert calculate_discount(age=30) == 0.1
def test_child_gets_no_discount(self):
"""ПАДАЕТ если дети получают скидку"""
assert calculate_discount(age=10) == 0.0
def test_discount_is_percentage(self):
"""ПАДАЕТ если скидка вне диапазона [0, 1]"""
discount = calculate_discount(age=50)
assert 0 <= discount <= 1
# Эти тесты НЕ нужны:
# def test_calculation_takes_less_than_1ms(self): # Flaky
# def test_discount_matches_competitor_pricing(self): # Не unit-тест
# def test_function_name_is_calculate_discount(self): # Деталь реализации
Вывод
Unit-тесты — это спецификация контракта функции.
Они должны падать когда:
- Функция не соответствует своему контракту
- Произошла регрессия
- Требования изменились
Они НЕ должны падать из-за случайности, внешних факторов или деталей реализации.
Хороший тест — это тест, который:
- Падает при действительной ошибке
- Не падает из-за мусора
- Быстро выполняется
- Понятен через 6 месяцев