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

Как понять, что написанный Unit test полностью готов?

2.0 Middle🔥 111 комментариев
#Архитектура и паттерны#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как понять, что Unit test готов

Unit test готов, когда он полностью проверяет поведение функции/метода, maintainable и не создаёт ложных срабатываний. Это не только наличие кода, но и качество.

Критерий 1: Покрытие всех путей выполнения

Тест должен проверить все ветви кода:

def calculate_discount(price: float, is_member: bool) -> float:
    """Считает цену со скидкой."""
    if is_member:
        return price * 0.9  # Скидка 10%
    else:
        return price

# ❌ Неполный тест (не проверяет обе ветви)
def test_calculate_discount():
    assert calculate_discount(100, True) == 90  # Только одна ветвь

# ✅ Полный тест
def test_calculate_discount_member():
    assert calculate_discount(100, True) == 90

def test_calculate_discount_non_member():
    assert calculate_discount(100, False) == 100

Проверка покрытия:

pip install pytest-cov
pytest --cov=app --cov-report=html

Цель: 90%+ покрытие для критического кода.

Критерий 2: Проверка граничных значений (Edge Cases)

def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("Division by zero")
    return a / b

# ✅ Тест с edge cases
class TestDivide:
    def test_normal_division(self):
        assert divide(10, 2) == 5
    
    def test_division_by_zero(self):
        with pytest.raises(ValueError):
            divide(10, 0)
    
    def test_negative_numbers(self):
        assert divide(-10, 2) == -5
    
    def test_division_by_negative(self):
        assert divide(10, -2) == -5
    
    def test_float_precision(self):
        assert abs(divide(10, 3) - 3.333333) < 0.0001
    
    def test_zero_divided_by_number(self):
        assert divide(0, 5) == 0

Критерий 3: Проверка исключений

Тест должен проверить все выбрасываемые исключения:

class InvalidEmailError(Exception):
    pass

def validate_email(email: str) -> str:
    if not email:
        raise ValueError("Email cannot be empty")
    if "@" not in email:
        raise InvalidEmailError("Invalid email format")
    return email.lower()

# ✅ Полная проверка исключений
class TestValidateEmail:
    def test_valid_email(self):
        assert validate_email("user@example.com") == "user@example.com"
    
    def test_empty_email_raises_value_error(self):
        with pytest.raises(ValueError, match="cannot be empty"):
            validate_email("")
    
    def test_invalid_format_raises_custom_error(self):
        with pytest.raises(InvalidEmailError, match="Invalid email"):
            validate_email("invalid-email")
    
    def test_converts_to_lowercase(self):
        assert validate_email("USER@EXAMPLE.COM") == "user@example.com"

Критерий 4: AAA паттерн (Arrange-Act-Assert)

Кажда строка теста должна быть явной:

# ❌ Плохо: логика смешана
def test_user_creation():
    user = User("Alice", "alice@ex.com")
    assert user.name == "Alice" and user.email == "alice@ex.com"

# ✅ Хорошо: явное разделение
def test_user_creation():
    # Arrange (подготовка)
    name = "Alice"
    email = "alice@example.com"
    
    # Act (выполнение)
    user = User(name, email)
    
    # Assert (проверка)
    assert user.name == name
    assert user.email == email

Критерий 5: Одно утверждение на концепцию

# ❌ Плохо: много проверок в одном тесте
def test_user():
    user = User("Alice", "alice@ex.com")
    assert user.name == "Alice"
    assert user.email == "alice@ex.com"
    assert user.is_active == True
    assert user.created_at is not None
    # Если один assert упадёт, остальные не выполнятся

# ✅ Хорошо: отдельные тесты
def test_user_initialization():
    user = User("Alice", "alice@example.com")
    assert user.name == "Alice"

def test_user_email():
    user = User("Alice", "alice@example.com")
    assert user.email == "alice@example.com"

def test_user_active_by_default():
    user = User("Alice", "alice@example.com")
    assert user.is_active == True

Критерий 6: Независимость и изоляция

Тесты не должны зависеть друг от друга:

# ❌ Плохо: зависимость от порядка
class TestDatabase:
    def test_1_insert(self):
        db.insert("Alice")
        assert len(db) == 1
    
    def test_2_query(self):
        # Полагается на test_1_insert
        result = db.query("Alice")
        assert result is not None

# ✅ Хорошо: каждый тест независим (с setup/teardown)
class TestDatabase:
    def setup_method(self):
        """Выполняется перед каждым тестом."""
        self.db = Database()
    
    def teardown_method(self):
        """Выполняется после каждого теста."""
        self.db.close()
    
    def test_insert(self):
        self.db.insert("Alice")
        assert len(self.db) == 1
    
    def test_query(self):
        self.db.insert("Alice")
        result = self.db.query("Alice")
        assert result is not None

Критерий 7: Использование fixtures

import pytest

@pytest.fixture
def user():
    """Fixture для создания пользователя."""
    return User("Alice", "alice@example.com")

@pytest.fixture
def admin():
    """Fixture для создания администратора."""
    return User("Admin", "admin@example.com", is_admin=True)

def test_user_name(user):
    assert user.name == "Alice"

def test_admin_permissions(admin):
    assert admin.is_admin == True

def test_user_and_admin(user, admin):
    assert user.is_admin == False
    assert admin.is_admin == True

Критерий 8: Проверка на flaky тесты

Flaky тесты не гарантируют одинаковый результат:

# ❌ Flaky: зависит от текущего времени
def test_user_created_today():
    user = User("Alice")
    assert user.created_at.date() == datetime.now().date()  # Может упасть в 00:00:00

# ✅ Стабильный: без зависимости от системного времени
from unittest.mock import patch

def test_user_created_with_timestamp():
    fixed_time = datetime(2025, 1, 1, 12, 0, 0)
    with patch('datetime.datetime') as mock_datetime:
        mock_datetime.now.return_value = fixed_time
        user = User("Alice")
        assert user.created_at == fixed_time

Критерий 9: Дескриптивные имена

# ❌ Плохо: неясное название
def test_user():
    pass

def test_validation():
    pass

# ✅ Хорошо: из названия ясно что проверяется
def test_user_creation_with_valid_email():
    pass

def test_user_creation_rejects_empty_email():
    pass

def test_email_validation_requires_at_symbol():
    pass

Критерий 10: Скорость выполнения

Единичный тест должен выполняться за миллисекунды:

# ❌ Медленный: делает реальный HTTP запрос
def test_api_request():
    response = requests.get("https://api.example.com/users")
    assert response.status_code == 200

# ✅ Быстрый: использует mock
from unittest.mock import patch

@patch('requests.get')
def test_api_request(mock_get):
    mock_get.return_value.status_code = 200
    response = requests.get("https://api.example.com/users")
    assert response.status_code == 200

Измерение скорости:

pytest --durations=10  # Показывает 10 самых медленных тестов

Критерий 11: Mock и Stub

Внешние зависимости должны мокироваться:

class UserService:
    def __init__(self, email_sender):
        self.email_sender = email_sender
    
    def register(self, email: str):
        self.email_sender.send(email, "Welcome!")
        return True

# ✅ Тест с mock
def test_register_sends_email():
    mock_email_sender = Mock()
    service = UserService(mock_email_sender)
    
    service.register("alice@example.com")
    
    mock_email_sender.send.assert_called_once_with(
        "alice@example.com", "Welcome!"
    )

Критерий 12: Наличие документации

def test_calculate_discount_for_members():
    """Тест проверяет что члены получают 10% скидку.
    
    Given: пользователь является членом
    When: вызывается calculate_discount
    Then: возвращается цена со скидкой 10%
    """
    price = 100
    discount = calculate_discount(price, is_member=True)
    assert discount == 90

Чеклист готовности Unit теста

  • Покрытие всех путей выполнения (>90% coverage)
  • Проверены граничные значения
  • Все исключения проверены
  • Используется AAA паттерн
  • Один assert на концепцию (или несколько связанных)
  • Тест независим от других
  • Используются fixtures для подготовки данных
  • Нет flaky тестов
  • Дескриптивное название
  • Выполняется < 1 сек
  • Внешние зависимости мокированы
  • Есть docstring
  • Пройдёт pytest --strict-markers
  • Документ зелёный (все assertions passed)

Unit test готов, когда он надёжен, быстр, понятен и полностью тестирует функцию.