Modern QA2026Типичные ошибки ИИ-генерированных тестов
Join

Course02 AI-Augmented Test Design

Cutting-edge · Chapter 02

Типичные ошибки ИИ-генерированных тестов

Updated Jul 2026

Почему ИИ-генерированные тесты ошибаются

ИИ-генерированные тесты ошибаются предсказуемым образом. Понимание этих режимов отказа позволяет вам мгновенно выявлять их при ревью, а не обнаруживать позже в CI или -- что ещё хуже -- в продакшене, когда тест проходит, несмотря на реальную ошибку.

Эти режимы отказа не случайны. Они вытекают из принципа работы LLM: они предсказывают статистически вероятный текст, а не логически корректный код. Это означает, что они создают паттерны, которые выглядят как тесты, но не работают как тесты.

Режим отказа 1: Тест-тавтология

Тест-тавтология тестирует мок, а не код. Он проверяет, что значение, которое вы сами явно задали, возвращается -- что всегда будет истинным, независимо от того, работает ли реальный код.

# THE TAUTOLOGY -- this tests nothing
def test_get_user(mock_db):
    mock_db.get_user.return_value = {"name": "Alice"}
    result = get_user(1)
    assert result["name"] == "Alice"  # This tests the mock, not the code

Почему ИИ генерирует это: LLM видит паттерн «настроить данные, вызвать функцию, проверить данные» и заполняет его, не задумываясь, осмысленно ли утверждение.

Как обнаружить: Для каждого утверждения проследите значение в обратном направлении. Если ожидаемое значение берётся напрямую из настройки самого теста (не проходя через продакшен-код), это тавтология.

Как исправить:

# FIXED: test the actual behavior
def test_get_user_returns_formatted_name(mock_db):
    mock_db.get_user.return_value = {"first_name": "Alice", "last_name": "Smith"}
    result = get_user(1)
    # Now we test that get_user() formats the name correctly
    assert result["display_name"] == "Alice Smith"
    assert result["initials"] == "AS"

Разновидности тавтологий

Тавтология-эхо:

def test_create_order(client):
    payload = {"product_id": "abc", "quantity": 2}
    response = client.post("/orders", json=payload)
    body = response.json()
    assert body["product_id"] == "abc"    # Just echoing the input
    assert body["quantity"] == 2          # Just echoing the input
    # Missing: assert body["status"] == "pending"   (actual business logic)
    # Missing: assert body["total"] == 19.98         (calculated value)
    # Missing: assert "id" in body                   (generated value)

Тавтология-прокидывание:

def test_transform_data(mock_api):
    mock_api.fetch.return_value = [1, 2, 3]
    result = transform_data()
    assert result == [1, 2, 3]   # If transform_data just returns the raw data,
                                  # this test catches nothing about the transform

Режим отказа 2: Только happy path

ИИ генерирует 10 тестов, и все -- для успешных сценариев. Никакой обработки ошибок, граничных случаев, отказов аутентификации.

# AI generates these:
def test_create_order_valid(): ...
def test_create_order_with_coupon(): ...
def test_create_order_express_shipping(): ...
def test_create_order_multiple_items(): ...
def test_create_order_with_notes(): ...

# Missing: what happens with invalid product ID?
# Missing: what happens with out-of-stock product?
# Missing: what happens with payment failure?
# Missing: what happens with expired auth token?
# Missing: what happens with zero quantity?

Почему ИИ генерирует это: LLM обучены на коде, где большинство примеров тестов -- это happy-path тесты. Статистический вес позитивных примеров превышает негативные.

Как обнаружить: Подсчитайте соотношение утверждений об успехе к утверждениям об ошибках. Если более 60% тестов проверяют status_code == 200/201, у вас смещение в сторону happy path.

Как исправить: Добавьте явную инструкцию в промпт:

For every happy-path test you generate, also generate:
- 1 test for missing required fields
- 1 test for invalid field types
- 1 test for authentication failure
- 1 test for authorization failure (wrong role)
- 1 test for resource-not-found

Режим отказа 3: Избыточно конкретное утверждение

Тест проверяет точный текст сообщения об ошибке, временные метки или автоматически сгенерированные идентификаторы, которые меняются между запусками.

# BRITTLE: depends on exact error message text
assert error.message == "Invalid email: 'notanemail' does not match RFC 5322 format"

# BRITTLE: depends on exact timestamp
assert order.created_at == "2026-02-09T10:30:00Z"

# BRITTLE: depends on auto-generated ID format
assert user.id == "usr_a1b2c3d4e5f6"

Почему ИИ генерирует это: LLM создаёт конкретные значения, потому что они выглядят как реалистичные тестовые утверждения. Она не анализирует, какие значения детерминированы, а какие генерируются во время выполнения.

Как исправить:

# RESILIENT: checks type and content, not exact string
assert isinstance(error, ValidationError)
assert "email" in str(error).lower()

# RESILIENT: checks existence and recency
assert order.created_at is not None
assert (datetime.now(UTC) - order.created_at).seconds < 5

# RESILIENT: checks format, not exact value
assert user.id is not None
assert user.id.startswith("usr_")
assert len(user.id) == 16

Режим отказа 4: Галлюцинированный API

ИИ изобретает методы, эндпоинты или параметры, которых не существует в вашей кодовой базе.

# AI invents methods that don't exist
result = client.orders.find_by_status("pending")  # find_by_status doesn't exist
user = UserService.get_or_create(email="test@test.com")  # get_or_create doesn't exist
response = client.patch("/api/v2/orders/1/cancel")  # this endpoint doesn't exist

Почему ИИ генерирует это: LLM видела похожие API в обучающих данных (Django get_or_create, Rails find_by_*) и применяет эти паттерны к вашей кодовой базе.

Как обнаружить: Всегда ищите в вашей кодовой базе любую функцию, метод или эндпоинт, на которые ссылается ИИ.

# Quick hallucination check
grep -rn "find_by_status" app/ --include="*.py"
grep -rn "get_or_create" app/ --include="*.py"
grep -rn "/cancel" app/routes/ --include="*.py"

Если grep ничего не возвращает, API галлюцинирован. Удалите тест или замените вызов на реальный API.

Как предотвратить: Включите реальные сигнатуры функций или список эндпоинтов в контекст промпта.

Режим отказа 5: Недетерминированный тест

Тест зависит от времени, случайных данных или внешнего состояния, которое меняется между запусками.

# FLAKY: depends on current time
def test_expiry_check():
    token = create_token(expires_in=3600)
    assert not token.is_expired()    # Passes now, fails if test takes > 1 hour

# FLAKY: depends on random data
def test_random_assignment():
    user = create_user()
    assert user.group in ["A", "B"]  # Passes most times, fails if random assigns "C"

# FLAKY: depends on database state from another test
def test_user_count():
    assert User.count() == 5  # Depends on exact state left by previous tests

Как исправить:

# FIXED: mock time explicitly
def test_expiry_check(freezer):
    freezer.move_to("2026-02-09T10:00:00Z")
    token = create_token(expires_in=3600)
    freezer.move_to("2026-02-09T10:30:00Z")  # 30 min later
    assert not token.is_expired()
    freezer.move_to("2026-02-09T11:01:00Z")  # 61 min later
    assert token.is_expired()

# FIXED: seed random
def test_random_assignment():
    random.seed(42)
    user = create_user()
    assert user.group == "A"  # Deterministic with seed

# FIXED: use relative assertions
def test_user_creation_increments_count():
    before = User.count()
    create_user(name="New User")
    assert User.count() == before + 1

Режим отказа 6: Тест без утверждений

Тест выполняет код, но никогда ничего не утверждает. Он лишь доказывает, что код не выбрасывает исключение.

# USELESS: no assertion
def test_process_order():
    order = create_order(product_id="abc", quantity=2)
    process_order(order)
    # Test passes as long as no exception is raised
    # But what if the order was not actually processed?

Как обнаружить: Ищите тестовые функции без операторов assert, raises или expect.

# Find assertion-free tests
grep -rn "def test_" tests/ | while read line; do
    func=$(echo "$line" | sed 's/.*def \(test_[a-z_]*\).*/\1/')
    file=$(echo "$line" | cut -d: -f1)
    if ! grep -A 20 "def $func" "$file" | grep -q "assert\|raises\|pytest.mark"; then
        echo "WARNING: $func in $file has no assertion"
    fi
done

Как исправить: Каждый тест должен утверждать что-то конкретное о результате.

Сводная таблица режимов отказа

Режим отказа Скорость обнаружения Частота Критичность
Тавтология Средняя (требует отслеживания) 15-20% ИИ-тестов Высокая
Только happy path Быстрая (подсчёт тестов на ошибки) 30-40% ИИ-наборов Высокая
Избыточная конкретность Быстрая (ищите литералы) 10-15% ИИ-тестов Средняя
Галлюцинированный API Средняя (требует grep) 5-10% ИИ-тестов Критическая
Недетерминированность Медленная (проявляется как нестабильность) 5-10% ИИ-тестов Высокая
Отсутствие утверждений Быстрая (автоматическая проверка) 5-8% ИИ-тестов Критическая

Один вопрос для ревью

Для каждого ИИ-сгенерированного теста задайте этот единственный вопрос:

«Если бы тестируемая функциональность была полностью сломана, упал бы этот тест?»

Если ответ «нет» или «не уверен», тест нуждается в переработке или удалении. Этот единственный вопрос выявляет тавтологии, тесты без утверждений и покрытие только happy path за один проход.

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

Ошибки ИИ-тестов предсказуемы и классифицируемы. Изучите шесть режимов отказа, и вы сможете ревьюить 30 ИИ-сгенерированных тестов за 15 минут с высокой уверенностью. Наиболее частые ошибки -- тавтологии и смещение в сторону happy path -- составляют более половины всех проблем и легче всего обнаруживаются.