Покрытие тестами, моки, assertion quality, edge cases в тестах
Код без тестов — это технический долг с процентами. Но плохие тесты хуже, чем их отсутствие.
Вопросы для review:
# ❌ Плохо: тест только на happy path
def test_add_to_cart():
cart = Cart()
cart.add(Product("Apple", 100))
assert cart.total == 100
# ✅ Хорошо: тесты на разные сценарии
def test_add_to_cart():
cart = Cart()
cart.add(Product("Apple", 100))
assert cart.total == 100
def test_add_to_cart_empty_product():
cart = Cart()
with pytest.raises(ValueError):
cart.add(None)
def test_add_to_cart_negative_quantity():
cart = Cart()
with pytest.raises(ValueError):
cart.add(Product("Apple", 100), quantity=-1)
def test_add_to_cart_duplicate():
cart = Cart()
cart.add(Product("Apple", 100))
cart.add(Product("Apple", 100))
assert cart.total == 200
assert cart.item_count == 1 # Тот же товар, не новый# ❌ Плохо: неясно, что ожидается
def test_calculate_discount():
result = calculate_discount(100, 0.1)
assert result # Что именно ожидается? True? 90? Не 0?
# ✅ Хорошо: конкретное значение
def test_calculate_discount():
result = calculate_discount(100, 0.1)
assert result == 90 # Ожидаем 90 после 10% скидки
# ✅ Отлично: с сообщением об ошибке
def test_calculate_discount():
result = calculate_discount(100, 0.1)
assert result == 90, f"Expected 90, got {result}"# ❌ Плохо: несколько проверок в одном тесте
def test_user_creation():
user = User("john", "john@example.com")
assert user.name == "john"
assert user.email == "john@example.com"
assert user.is_active is True
assert user.created_at is not None
# При провале непонятно, какая именно проверка не прошла
# ✅ Хорошо: отдельные тесты
def test_user_name():
user = User("john", "john@example.com")
assert user.name == "john"
def test_user_email():
user = User("john", "john@example.com")
assert user.email == "john@example.com"
def test_user_is_active_by_default():
user = User("john", "john@example.com")
assert user.is_active is TrueИспользуйте моки для:
datetime.now())# ❌ Плохо: реальный HTTP запрос в тесте
def test_fetch_user():
user = fetch_user(123) # Реальный запрос к API!
assert user.name == "John"
# ✅ Хорошо: мок внешнего API
from unittest.mock import patch
def test_fetch_user():
with patch('myapp.api.requests.get') as mock_get:
mock_get.return_value.json.return_value = {'id': 123, 'name': 'John'}
user = fetch_user(123)
assert user.name == "John"
mock_get.assert_called_once_with('https://api.example.com/users/123')# ❌ Плохо: слишком много моков, тестируем моки а не логику
@patch('module.Database')
@patch('module.Cache')
@patch('module.Logger')
@patch('module.Config')
def test_something(mock_config, mock_logger, mock_cache, mock_db):
# Тест стал сложнее, чем продакшен код
...
# ✅ Хорошо: моки только внешних зависимостей
def test_calculate_total():
cart = Cart() # Реальный объект
cart.add(Product("Apple", 100))
# Мокируем только внешний сервис
with patch('myapp.tax_service.calculate_tax') as mock_tax:
mock_tax.return_value = 10
total = cart.total_with_tax()
assert total == 110| Тип | Edge cases |
|---|---|
| Числа | 0, -1, 1, очень большие, очень маленькие, float precision |
| Строки | "", 1 символ, очень длинная, special chars, unicode |
| Списки | [], [1 элемент], [очень много], None |
| Даты | Високосный год, граница года, DST переход |
| Сеть | Timeout, 500 error, пустой response, malformed JSON |
# Пример: тестирование функции парсинга
def test_parse_int():
# Нормальные случаи
assert parse_int("42") == 42
assert parse_int("-10") == -10
# Edge cases
assert parse_int("0") == 0
assert parse_int(" 42 ") == 42 # Пробелы
# Ошибки
with pytest.raises(ValueError):
parse_int("not a number")
with pytest.raises(ValueError):
parse_int("")
with pytest.raises(ValueError):
parse_int(None)# ❌ Плохо: неясно, что тестирует
def test_user():
...
def test_calc():
...
def test_stuff():
...
# ✅ Хорошо: имя описывает сценарий
def test_user_creation_with_valid_email():
...
def test_calculate_discount_returns_correct_amount():
...
def test_login_fails_with_invalid_password():
...
# ✅ Отлично: формат Given-When-Then
def test_given_valid_cart_when_calculate_total_then_returns_sum():
...# ❌ Плохо: всё перемешано
def test_order():
user = User("john") # Arrange
order = Order(user)
order.add(Product("Apple", 100)) # Act
total = order.total
assert total == 100 # Assert
user.email = "new@example.com" # Снова Arrange?
order.update_user(user) # Снова Act?
assert order.user.email == "new@example.com" # Снова Assert?
# ✅ Хорошо: чёткая структура
def test_order_total():
# Arrange
user = User("john")
order = Order(user)
order.add(Product("Apple", 100))
# Act
total = order.total
# Assert
assert total == 100
def test_order_user_update():
# Arrange
user = User("john", "old@example.com")
order = Order(user)
new_user = User("john", "new@example.com")
# Act
order.update_user(new_user)
# Assert
assert order.user.email == "new@example.com"# ❌ Плохо: зависит от текущего времени
def test_expiration():
token = Token(expires_in=3600)
assert token.is_expired() is False # Может упасть через час!
# ✅ Хорошо: контролируемое время
from freezegun import freeze_time
@freeze_time("2024-01-01 12:00:00")
def test_expiration():
token = Token(expires_in=3600)
assert token.is_expired() is False
# ✅ Или передача времени явно
def test_expiration():
token = Token(expires_at=datetime(2024, 1, 1, 13, 0, 0))
with freeze_time("2024-01-01 12:00:00"):
assert token.is_expired() is False# ❌ Плохо: тесты зависят от порядка выполнения
shared_state = []
def test_first():
shared_state.append(1)
assert len(shared_state) == 1
def test_second():
shared_state.append(2)
assert len(shared_state) == 2 # Зависит от test_first!
# ✅ Хорошо: каждый тест независим
def test_first():
state = []
state.append(1)
assert len(state) == 1
def test_second():
state = []
state.append(2)
assert len(state) == 2Ключевая мысль: Хороший тест — это документация, которая всегда актуальна. Он описывает ожидаемое поведение и защищает от регрессии.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.