Типичные ошибки ИИ-генерированных тестов
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 -- составляют более половины всех проблем и легче всего обнаруживаются.