AI-генерация тестов: системные слепые зоны

#testing#ai#quality-assurance#frontend

TL;DR: AI-генерация тестов пропускает целые категории багов из-за структурных ограничений LLM. В 62.5% случаев тесты не покрывают реальный класс ошибки, при этом пропуски систематичны и предсказуемы.

Введение: эпоха AI-assisted testing

Современные LLM типа Claude/GPT стали неотъемлемой частью workflow senior-разработчиков. Особенно при:

Но наш анализ 16 реальных багов из SWE-bench показал: AI-тесты имеют предсказуемые blind spots. Они не случайны — это прямое следствие архитектурных ограничений языковых моделей.

Основные паттерны пропусков

1. Cascade-blindness

Типичный кейс: AI меняет метод A, пишет тест для метода A, но не анализирует impact на методы B, C, D.

Пример из sympy:

# Было
def nthroot_mod(a, n, p, all_roots=False):
    if not isprime(p):
        raise NotImplementedError("Not implemented for composite p")

# Стало
def nthroot_mod(a, n, p, all_roots=False):
    if not isprime(p):
        return _nthroot_mod_composite(a, n, p)

AI-тест:

def test_nthroot_mod_prime():
    assert nthroot_mod(2, 3, 5) == ...  # Тестирует только prime case

Пропущенный тест:

def test_nthroot_mod_composite():
    assert nthroot_mod(1, 3, 15) is not None  # Проверяет новый composite case

2. Circular mocking

LLM создают моки, которые повторяют их же предположения:

// Production bug:
interface APIResponse {
  userId: string;  // Реальный API
}

// AI-generated code:
function processUser(data: { user: { id: string } }) {
  return data.user.id;
}

// AI-generated test:
jest.mock('api', () => ({
  fetchUser: () => ({ user: { id: '123' } })  // Тот же некорректный контракт
}));

// Тест проходит, но production падает

3. Boundary blindness

AI редко тестирует edge cases:

// Пропущенные кейсы:
- Пустые массивы
- null/undefined
- Пограничные значения
- Максимальные числа

Практические решения

1. Blast radius analysis

Автоматизируем поиск зависимостей:

# AST-анализ для Python
import ast

def find_dependents(code, target_func):
    tree = ast.parse(code)
    dependents = []
    
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            if hasattr(node.func, 'id') and node.func.id == target_func:
                dependents.append(node)
    return dependents

2. Mutation testing

Добавляем мутации для проверки тестов:

// Jest + Stryker
import { mutate } from 'stryker';

test('should handle null input', () => {
  const original = (x) => x?.toUpperCase();
  const mutated = mutate(original, { nullInput: true });
  expect(mutated(null)).toBeNull();
});

3. Контрактное тестирование

Фиксируем API-контракты:

// Pact-тест
import { Pact } from '@pact-foundation/pact';

describe('API Contract', () => {
  beforeAll(() => {
    new Pact()
      .addInteraction({
        state: 'user exists',
        uponReceiving: 'GET /user',
        willRespondWith: {
          status: 200,
          body: {
            userId: Matchers.string()  // Явное требование
          }
        }
      })
      .start();
  });
});

Заключение

AI-тестирование — мощный инструмент, но требует:

  1. Дополнительной верификации через mutation testing
  2. Анализа графа зависимостей
  3. Явного контрактного тестирования

Инструменты вроде Optinum автоматизируют поиск blind spots, но senior-разработчик должен понимать системные ограничения AI.

Key takeaways:

Для глубокого погружения рекомендую:

  1. Изучить SWE-bench датасет
  2. Поэкспериментировать с Optinum
  3. Внедрить mutation testing в CI/CD

Источник: https://dev.to/anhnguyensynctree/ai-writes-your-tests-heres-what-it-systematically-misses-3a38