← Назад к вопросам
Как понять, что написанный 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 готов, когда он надёжен, быстр, понятен и полностью тестирует функцию.