ЧАСТЬ II: ФУНДАМЕНТ МАСТЕРСТВА

Навык #6: Тестирование

Глава 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):

  1. Пишете тест
  2. Пишете код вручную
  3. Запускаете тест
  4. Рефакторите

Сейчас (TDD + AI):

  1. Пишете тест
  2. Даёте тест AI → AI генерирует код
  3. Запускаете тест
  4. Если не прошёл → корректируете промпт → 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 (лёгкую).

  1. Напишите тесты на основе примеров
  2. Дайте тесты AI
  3. AI генерирует решение
  4. Проверьте на LeetCode

Задание 3: Mock внешних API

Создайте функцию, которая делает HTTP запрос к внешнему API.

Напишите тест с использованием mock (без реальных запросов).


Ключевые выводы главы

Тесты критичны с AI: AI генерирует код, вы проверяете тестами

Виды тестов: Unit (много, быстрые) → Integration → E2E (мало, медленные)

TDD: Сначала тест, потом код

TDD + AI = мощь: Вы пишете тест, AI пишет код

Coverage: Стремитесь к 80%+

Mock: Имитация внешних зависимостей

Workflow: Тест → Промпт с тестами → AI генерирует → Запуск → Итерация

С AI: Тестирование стало ещё важнее, но и проще (AI пишет тесты тоже!)


Следующая глава: Git и контроль версий — коллаборация критична