Глава 9. Навык #6: Тестирование
"TDD с AI — мощная комбинация. Вы пишете тест, AI пишет код, который проходит тест."
9.1. AI генерирует код, вы проверяете
Почему тестирование критично с AI
AI пишет код быстро. Но не всегда правильно.
Сценарий:
Вы: AI, создай функцию для валидации email
AI: [генерирует код]
Вы: Отлично! [копируете в проект]
Через неделю в продакшне:
is_valid_email("user@") # True — но это невалидный email!
Проблема: Вы не проверили код тестами.
Тесты — это страховка
Без тестов:
- Вы не знаете, работает ли код на всех случаях
- Изменения могут сломать существующий функционал
- Баги находятся на продакшне
С тестами:
- Уверенность, что код работает
- Регрессия ловится автоматически
- Рефакторинг безопасен
С AI тесты ещё важнее!
9.2. Виды тестов: unit, integration, e2e
Пирамида тестирования
/\
/ \ E2E тесты (мало, медленные, но важные)
/____\
/ \ Integration тесты (средне)
/________\
/ \ Unit тесты (много, быстрые)
/____________\
Unit тесты — Тестируют одну функцию
Что: Тестируют отдельную функцию или метод изолированно.
Пример:
# Функция
def add(a, b):
return a + b
# Unit тест
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
Преимущества:
- Быстрые (миллисекунды)
- Простые в написании
- Точно показывают, где проблема
Integration тесты — Тестируют взаимодействие
Что: Тестируют, как несколько компонентов работают вместе.
Пример:
# Integration тест (API + БД)
def test_create_user_integration():
# Создаём пользователя через API
response = client.post("/users", json={
"name": "Alice",
"email": "alice@example.com"
})
assert response.status_code == 201
# Проверяем, что пользователь в БД
user = db.query(User).filter_by(email="alice@example.com").first()
assert user is not None
assert user.name == "Alice"
E2E (End-to-End) тесты — Тестируют весь flow
Что: Тестируют пользовательский сценарий от начала до конца.
Пример (веб-приложение):
def test_user_registration_e2e():
# 1. Открыть страницу регистрации
browser.get("https://example.com/register")
# 2. Заполнить форму
browser.find_element_by_id("name").send_keys("Alice")
browser.find_element_by_id("email").send_keys("alice@example.com")
browser.find_element_by_id("password").send_keys("SecurePass123!")
# 3. Нажать кнопку
browser.find_element_by_id("submit").click()
# 4. Проверить редирект на главную
assert browser.current_url == "https://example.com/dashboard"
# 5. Проверить приветствие
welcome = browser.find_element_by_class("welcome-message").text
assert "Welcome, Alice" in welcome
9.3. Как писать тестовые случаи
Структура теста: Arrange-Act-Assert
def test_calculate_discount():
# Arrange — подготовка
price = 100
discount_percent = 20
# Act — действие
result = calculate_discount(price, discount_percent)
# Assert — проверка
assert result == 80
Что тестировать
1. Happy path — нормальный случай:
def test_divide_normal():
assert divide(10, 2) == 5
2. Edge cases — граничные случаи:
def test_divide_edge_cases():
assert divide(10, 0) raises ValueError # деление на ноль
assert divide(0, 10) == 0 # ноль делить на число
assert divide(1, 3) == 0.333... # дробный результат
3. Invalid input — невалидные данные:
def test_divide_invalid():
assert divide("10", 2) raises TypeError # строка вместо числа
assert divide(None, 2) raises TypeError # None
Примеры тестовых случаев
Функция: is_strong_password(password)
# Happy path
def test_strong_password():
assert is_strong_password("Abc123!@") == True
# Edge cases
def test_weak_passwords():
assert is_strong_password("abc123") == False # нет заглавной
assert is_strong_password("Abcdefg!") == False # нет цифры
assert is_strong_password("Ab1!") == False # короткий
assert is_strong_password("") == False # пустой
# Invalid input
def test_password_invalid_type():
with pytest.raises(TypeError):
is_strong_password(None)
9.4. Test-Driven Development (TDD)
Что такое TDD
TDD (Test-Driven Development) — методология, где тест пишется ДО кода.
Цикл TDD:
1. Red — Пишем тест (он падает, т.к. функции нет)
2. Green — Пишем код, чтобы тест прошёл
3. Refactor — Улучшаем код, тесты проходят
Пример TDD
Задача: Создать функцию get_full_name(first_name, last_name)
Шаг 1: Red — пишем тест
def test_get_full_name():
assert get_full_name("John", "Doe") == "John Doe"
Тест падает — функции get_full_name нет.
Шаг 2: Green — пишем минимальный код
def get_full_name(first_name, last_name):
return f"{first_name} {last_name}"
Тест проходит! ✅
Шаг 3: Добавляем edge cases
def test_get_full_name_edge_cases():
# Пустые строки
assert get_full_name("", "") == ""
# Один параметр пустой
assert get_full_name("John", "") == "John"
assert get_full_name("", "Doe") == "Doe"
# С пробелами
assert get_full_name(" John ", " Doe ") == "John Doe"
Тесты падают.
Шаг 4: Улучшаем код
def get_full_name(first_name, last_name):
first = first_name.strip()
last = last_name.strip()
if not first and not last:
return ""
elif not first:
return last
elif not last:
return first
else:
return f"{first} {last}"
Тесты проходят! ✅
9.5. TDD с AI — мощная комбинация
Workflow: Тест → AI → Проверка
Раньше (традиционный TDD):
- Пишете тест
- Пишете код вручную
- Запускаете тест
- Рефакторите
Сейчас (TDD + AI):
- Пишете тест
- Даёте тест AI → AI генерирует код
- Запускаете тест
- Если не прошёл → корректируете промпт → AI улучшает код
Ускорение в 10 раз!
Пример: TDD + AI
Задача: Функция для проверки, является ли число простым.
Шаг 1: Пишем тесты
def test_is_prime():
# Простые числа
assert is_prime(2) == True
assert is_prime(3) == True
assert is_prime(5) == True
assert is_prime(17) == True
# Не простые
assert is_prime(1) == False
assert is_prime(4) == False
assert is_prime(10) == False
# Edge cases
assert is_prime(0) == False
assert is_prime(-5) == False
Шаг 2: Промпт для AI
AI, создай функцию is_prime(n: int) -> bool,
которая проверяет, является ли число простым.
Функция должна проходить следующие тесты:
[вставляем код тестов]
Оптимизируй для больших чисел.
Шаг 3: AI генерирует код
def is_prime(n: int) -> bool:
if n <= 1:
return False
if n == 2:
return True
if n % 2 == 0:
return False
# Проверяем только до sqrt(n)
for i in range(3, int(n**0.5) + 1, 2):
if n % i == 0:
return False
return True
Шаг 4: Запускаем тесты
pytest test_prime.py
Все тесты проходят! ✅
Если бы не прошли:
AI, тесты не прошли. Вот ошибки:
[вставляем вывод pytest]
Исправь код.
9.6. Покрытие кода тестами
Что такое покрытие (coverage)
Coverage — процент кода, который выполняется при запуске тестов.
Пример:
def divide(a, b):
if b == 0: # Строка 1
raise ValueError # Строка 2
return a / b # Строка 3
Если тест:
def test_divide():
assert divide(10, 2) == 5
Coverage:
- Строка 1: выполнена ✅
- Строка 2: НЕ выполнена ❌ (не тестировали деление на 0)
- Строка 3: выполнена ✅
Покрытие: 66% (2 из 3 строк)
Добавляем тест:
def test_divide_by_zero():
with pytest.raises(ValueError):
divide(10, 0)
Покрытие: 100% ✅
Инструменты
Python:
pip install pytest-cov
pytest --cov=myapp tests/
Цель: Стремиться к 80%+ coverage.
100% не всегда нужно, но 80%+ — хороший показатель.
9.7. Мокирование и стабы
Проблема: внешние зависимости
Функция отправляет email:
def send_welcome_email(user_email):
smtp_client.send(
to=user_email,
subject="Welcome!",
body="Welcome to our platform"
)
Проблема при тестировании:
- Нужен SMTP сервер
- Отправляются реальные emails (плохо для тестов!)
- Медленно
Решение: Mock (имитация)
Mock — фейковый объект, который имитирует реальный.
from unittest.mock import Mock
def test_send_welcome_email():
# Создаём mock SMTP клиента
mock_smtp = Mock()
# Подменяем реальный клиент на mock
smtp_client = mock_smtp
# Вызываем функцию
send_welcome_email("user@example.com")
# Проверяем, что send был вызван с правильными параметрами
mock_smtp.send.assert_called_once_with(
to="user@example.com",
subject="Welcome!",
body="Welcome to our platform"
)
Преимущества:
- Быстро (нет реальных запросов)
- Изолированно (не зависит от внешних систем)
- Можно тестировать edge cases (ошибка сети и т.д.)
Пример с API
import requests
from unittest.mock import patch
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
# Тест с mock
@patch('requests.get')
def test_get_user_data(mock_get):
# Настраиваем mock ответ
mock_get.return_value.json.return_value = {
"id": 1,
"name": "Alice"
}
# Вызываем функцию
result = get_user_data(1)
# Проверяем
assert result["name"] == "Alice"
mock_get.assert_called_once_with("https://api.example.com/users/1")
9.8. Практика: TDD workflow с AI
Упражнение: Функция расчёта возраста
Задача: Создать функцию calculate_age(birth_date: str) -> int
Шаг 1: Напишите тесты (самостоятельно)
def test_calculate_age():
# TODO: добавьте тестовые случаи
pass
Шаг 2: Дайте тесты AI
AI, создай функцию calculate_age(birth_date: str) -> int,
которая проходит следующие тесты:
[вставьте тесты]
Шаг 3: Запустите тесты
Шаг 4: Если не прошли — улучшите промпт
Пример тестов
from datetime import date
def test_calculate_age():
# Нормальные случаи
assert calculate_age("2000-01-01") == 25 # (если сейчас 2025)
# Edge cases
assert calculate_age("2025-01-01") == 0 # родился в этом году
# Invalid
with pytest.raises(ValueError):
calculate_age("invalid-date")
with pytest.raises(ValueError):
calculate_age("2030-01-01") # дата в будущем
9.9. Упражнения
Задание 1: Добавьте тесты к AI-коду
Попросите AI создать функцию (любую).
Затем вы напишите тесты для этой функции.
Запустите — все ли проходят?
Задание 2: TDD с AI
Выберите задачу с LeetCode (лёгкую).
- Напишите тесты на основе примеров
- Дайте тесты AI
- AI генерирует решение
- Проверьте на LeetCode
Задание 3: Mock внешних API
Создайте функцию, которая делает HTTP запрос к внешнему API.
Напишите тест с использованием mock (без реальных запросов).
Ключевые выводы главы
✅ Тесты критичны с AI: AI генерирует код, вы проверяете тестами
✅ Виды тестов: Unit (много, быстрые) → Integration → E2E (мало, медленные)
✅ TDD: Сначала тест, потом код
✅ TDD + AI = мощь: Вы пишете тест, AI пишет код
✅ Coverage: Стремитесь к 80%+
✅ Mock: Имитация внешних зависимостей
✅ Workflow: Тест → Промпт с тестами → AI генерирует → Запуск → Итерация
✅ С AI: Тестирование стало ещё важнее, но и проще (AI пишет тесты тоже!)
Следующая глава: Git и контроль версий — коллаборация критична