Перейти к содержимому
arckep.ru — все нейросети в одном месте без VPN Перейти
>AISTUDY_
Поддержать
AUTHORСвежий выпуск №024 → Куда внедрять агентов: фронт или тыл

Модуль 2.9 · Урок 1

Урок 1: TDD с агентом

Практика
2.9 / Урок 1 из 4

Чему вы научитесь

  • Применять Test-Driven Development вместе с AI-агентами
  • Писать тесты, которые служат спецификацией для агента
  • Создавать workflow «тест → реализация → итерация»
  • Использовать Claude Code для генерации кода под готовые тесты
  • Отлаживать код совместно с агентом через красный-зелёный-рефакторинг цикл

Почему TDD + AI работает идеально

Проблема: когда вы просто просите агента “написать функцию”, результат неопределённый. Агент может неправильно понять требования, использовать неподходящий подход, накопить ненужный техдолг.

Решение: TDD даёт агенту явную спецификацию в виде тестов. Вместо расплывчатого описания агент видит конкретные примеры входных и выходных данных. Это снижает неоднозначность на 90%.

graph LR
    A["1. Вы пишете<br/>тест"] -->|ясная спецификация| B["2. Агент пишет<br/>реализацию"]
    B -->|запускаем| C["3. Тест красный<br/>или зелёный?"]
    C -->|красный| D["4. Агент<br/>фиксит"]
    D --> C
    C -->|зелёный| E["5. Рефакторинг<br/>вместе"]
    E --> F["[+] Готовый код"]

Часть 1: Python + pytest

Сценарий: валидация email-адреса с дополнительными бизнес-правилами.

Шаг 1: Пишем тест

Файл test_email_validator.py:

import pytest
from email_validator import validate_email, EmailNotValidError
class TestEmailValidator:
    """Тесты для валидатора email"""

    def test_valid_simple_email(self):
        """Простой валидный email"""
        result = validate_email("user@example.com")
        assert result == "user@example.com"

    def test_valid_email_with_plus(self):
        """Email с + (Gmail-стиль)"""
        result = validate_email("user+tag@gmail.com")
        assert result == "user+tag@gmail.com"

    def test_reject_no_at_symbol(self):
        """Отклоняем email без @"""
        with pytest.raises(EmailNotValidError):
            validate_email("userexample.com")

    def test_reject_no_domain(self):
        """Отклоняем email без домена после @"""
        with pytest.raises(EmailNotValidError):
            validate_email("user@")

    def test_reject_multiple_at(self):
        """Отклоняем email с двумя @"""
        with pytest.raises(EmailNotValidError):
            validate_email("user@@example.com")

    def test_lowercase_transformation(self):
        """Приводим email в нижний регистр"""
        result = validate_email("User@EXAMPLE.COM")
        assert result == "user@example.com"

    def test_reject_disposable_domain(self):
        """Отклоняем одноразовые домены"""
        with pytest.raises(EmailNotValidError):
            validate_email("user@tempmail.com")

    def test_reject_spaces(self):
        """Отклоняем email с пробелами"""
        with pytest.raises(EmailNotValidError):
            validate_email("user @example.com")

Шаг 2: Даём код агенту

Команда для Claude Code:

claude "Напиши реализацию для email_validator.py.
Требования:
- Импортируй EmailNotValidError из email_validator (from email_validator import EmailNotValidError)
- Функция validate_email(email: str) -> str
- Должна пройти все тесты в test_email_validator.py
- Используй встроенные решения для базовой валидации
- Добавь список disposable_domains для проверки временных почт"

Шаг 3: Агент пишет код

Ожидаемый результат email_validator.py:

import re
from typing import Set

# Список распространённых одноразовых доменов
DISPOSABLE_DOMAINS: Set[str] = {
    "tempmail.com",
    "10minutemail.com",
    "guerrillamail.com",
    "mailinator.com",
    "temp-mail.org",
    "throwaway.email",
}
# EmailNotValidError импортируется из email_validator
def validate_email(email: str) -> str:
    """
    Валидирует email и возвращает нормализованный адрес.

    Args:
        email: Email-адрес для проверки

    Returns:
        Нормализованный (lowercase) email

    Raises:
        EmailNotValidError: Если email невалидный
    """
    # Проверка на пробелы
    if " " in email:
        raise EmailNotValidError("Email содержит пробелы")

    # Базовая проверка на наличие @
    at_count = email.count("@")
    if at_count == 0:
        raise EmailNotValidError("Email должен содержать @")
    if at_count > 1:
        raise EmailNotValidError("Email содержит больше одного @")

    # Разбиваем на локальную часть и домен
    local_part, domain = email.rsplit("@", 1)

    # Проверка на пустую локальную часть
    if not local_part:
        raise EmailNotValidError("Email должен содержать часть перед @")

    # Проверка на пустой домен
    if not domain or "." not in domain:
        raise EmailNotValidError("Домен должен содержать точку")

    # Проверка на одноразовый домен
    domain_lower = domain.lower()
    if domain_lower in DISPOSABLE_DOMAINS:
        raise EmailNotValidError(f"Домен {domain_lower} в чёрном списке")

    # Нормализация (приведение в нижний регистр)
    normalized = email.lower()

    return normalized

Шаг 4: Запускаем тесты

pytest test_email_validator.py -v

Ожидаемый результат:

test_email_validator.py::TestEmailValidator::test_valid_simple_email PASSED
test_email_validator.py::TestEmailValidator::test_valid_email_with_plus PASSED
test_email_validator.py::TestEmailValidator::test_reject_no_at_symbol PASSED
test_email_validator.py::TestEmailValidator::test_reject_no_domain PASSED
test_email_validator.py::TestEmailValidator::test_reject_multiple_at PASSED
test_email_validator.py::TestEmailValidator::test_lowercase_transformation PASSED
test_email_validator.py::TestEmailValidator::test_reject_disposable_domain PASSED
test_email_validator.py::TestEmailValidator::test_reject_spaces PASSED

============= 8 passed in 0.15s =============

Часть 2: JavaScript + Jest

Другой пример: валидация API-ключа.

Тесты

Файл apiKeyValidator.test.js:

const { validateApiKey, InvalidApiKeyError } = require('./apiKeyValidator');

describe('API Key Validator', () => {
  test('accepts valid 32-char hex key', () => {
    const key = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6';
    expect(validateApiKey(key)).toBe(key.toUpperCase());
  });

  test('rejects key shorter than 32 chars', () => {
    expect(() => validateApiKey('short')).toThrow(InvalidApiKeyError);
  });

  test('rejects non-hex characters', () => {
    expect(() => validateApiKey('g1g2g3g4e5f6a7b8c9d0e1f2a3b4c5d6')).toThrow(InvalidApiKeyError);
  });

  test('normalizes to uppercase', () => {
    const key = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6';
    expect(validateApiKey(key)).toBe(key.toUpperCase());
  });

  test('rejects keys with spaces', () => {
    expect(() => validateApiKey('a1b2c3d4 e5f6a7b8c9d0e1f2a3b4c5d6')).toThrow(InvalidApiKeyError);
  });

  test('rejects keys with special characters', () => {
    expect(() => validateApiKey('a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6')).toThrow(InvalidApiKeyError);
  });
});

Реализация

Файл apiKeyValidator.js:

class InvalidApiKeyError extends Error {
  constructor(message) {
    super(message);
    this.name = 'InvalidApiKeyError';
  }
}

function validateApiKey(key) {
  // Проверка на пробелы
  if (key.includes(' ')) {
    throw new InvalidApiKeyError('API ключ содержит пробелы');
  }

  // Проверка длины
  if (key.length !== 32) {
    throw new InvalidApiKeyError('API ключ должен быть ровно 32 символа');
  }

  // Проверка на hex-формат (только 0-9 и A-F)
  const hexRegex = /^[0-9a-fA-F]{32}$/;
  if (!hexRegex.test(key)) {
    throw new InvalidApiKeyError('API ключ должен содержать только hex-символы (0-9, A-F)');
  }

  return key.toUpperCase();
}

module.exports = { validateApiKey, InvalidApiKeyError };

Часть 3: Практический workflow в команде

Сценарий: исправление багов через TDD

1. QA-инженер находит баг: "функция crash'ит на null-значениях"

2. Разработчик пишет тест:
   test_should_handle_null_gracefully() {
     assert parse(null) == DEFAULT_VALUE
   }

3. Разработчик передаёт Claude Code:
   "Напиши реализацию parse(), чтобы прошёл тест test_should_handle_null_gracefully"

4. Claude пишет код, тест проходит

5. Разработчик просит рефакторинг:
   "Оптимизируй это для O(n) вместо O(n²), тесты не должны сломаться"

6. Итоговый код: баг исправлен, есть регрессионный тест

Таблица: TDD vs обычный подход

ПараметрTDD + AIОбычный подход
Ясность требованийТесты = спецификацияРасплывчатое описание
Качество AI-кодаВысокое (видит примеры)Низкое (гадает)
Регрессионные багиЗащита тестамиНет защиты
ДокументацияТесты служат примерамиНужна отдельно
Time-to-valueБыстро (целевой код)Долго (много переписываний)
Интеграция агентаПростая (агент → зелёные тесты)Сложная (много уточнений)

Антипаттерн: YOLO-код

[-] Неправильно:

# Просто просим агента писать
claude "напиши валидатор для email"

# Результат: неопределённое поведение, неясна спецификация

[+] Правильно:

# Даём тесты как спецификацию
claude "напиши валидатор email_validator.py, чтобы прошли тесты в test_email_validator.py"

# Результат: точный код, проходит все проверки

Попробуйте сами

Задача: Написать валидатор для номера телефона.

  1. Создайте файл test_phone_validator.py с 6-8 тестами:

    • Валидный номер в формате +7 XXX XXX-XX-XX
    • Отклонить номер без +7
    • Отклонить с пробелами в начале/конце
    • Отклонить с недостаточным количеством цифр
    • Привести к нормализованному формату (удалить пробелы и дефисы)
  2. Дайте Claude Code команду:

    claude "Напиши phone_validator.py с функцией validate_phone(phone: str) -> str,
    которая пройдёт все тесты в test_phone_validator.py"
  3. Запустите pytest test_phone_validator.py -v

  4. Если тесты не прошли, дайте агенту фидбэк:

    claude "Тесты не прошли с ошибкой: [ERROR].
    Исправь реализацию, сохранив API функции"
  5. Повторяйте, пока все тесты не будут зелёными.

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

  1. Тесты = спецификация: Чем точнее тесты, тем точнее код агента
  2. Red-Green-Refactor: Это работает и с AI: напиши тест → агент пишет код → рефакторь вместе
  3. Итерация через feedback: Не ожидайте идеального кода с первого раза, используйте результаты тестов как feedback
  4. Регрессионная защита: Тесты защищают от багов, когда агент (или вы) меняет код позже
  5. Практический опыт: TDD с AI-агентами значительно снижает неоднозначность требований и улучшает качество кода по сравнению с прямым промптом «напиши функцию»

Следующий урок

В уроке 2 мы поговорим о Code Review с агентом: как использовать Claude для автоматического анализа PR, что агент может найти, а что нет, и как настроить автоматический pre-review перед human review.

Мы размещаем рекламу, так как это позволяет нам готовить для вас свежие материалы и покрывать наши расходы. Рекламодателей выбираем адекватных.

Скачать урок

Есть идея или нашли ошибку?

// Обсуждение

Можно писать анонимно. Укажите email, чтобы получать уведомления об ответах.