← Назад к вопросам

При каких условиях должны падать 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)

Тест должен падать тогда и только тогда, когда:

  1. ✅ Код нарушает свой контракт (interface)
  2. ✅ Код регрессирует (перестаёт делать что-то, что делал раньше)
  3. ✅ Требования изменились, и код не соответствует новым требованиям

Тест НЕ должен падать:

  1. ❌ Из-за внешних факторов (время, сеть, random)
  2. ❌ Из-за порядка выполнения тестов
  3. ❌ Из-за деталей реализации (которые могут меняться)
  4. ❌ Из-за того, что мокировали неправильно

Пример правильного набора тестов

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 месяцев