"Портрет Луки Пачоли" (Якопо де Барбари, 1495)
"Портрет Луки Пачоли" (Якопо де Барбари, 1495)
Меланхолия тестировщика: почему метрики врут (Часть 1)
Альбрехт Дюрер, «Меланхолия I», 1514 Крылатый гений сидит среди инструментов. Циркуль, весы, молоток...
habr.com
EVA: Методология. Как оценивать качество тестов, а не их количество (Часть 2)
Ян ван Эйк. Портрет четы Арнольфини, 1434 Часть 2 из 3. [Первая часть - постановка проблемы] Меланхо...
habr.com

На картине Якопо де Барбари 1495 года францисканский монах Лука Пачоли демонстрирует ученику геометрическую фигуру. На столе перед ним инструменты точного измерения: циркуль, угольник, грифельная доска с чертежами. Слева висит прозрачный ромбокубооктаэдр, наполовину заполненный водой. Справа лежит закрытая книга, скорее всего его собственная «Сумма арифметики».

Пачоли не изобрёл двойную бухгалтерскую запись. Её использовали венецианские купцы задолго до него. Но именно он систематизировал разрозненные практики в воспроизводимый метод. Записал правила так, чтобы любой мог им следовать и получать предсказуемый результат. До Пачоли учёт был искусством, которое передавалось от мастера к подмастерью. После него стал технологией, которую можно изучить по книге.

Прозрачный многогранник на картине не случаен. Это символ того, к чему стремился Пачоли: сделать скрытое видимым, неявное явным, интуитивное измеримым. Ромбокубооктаэдр можно построить по точным правилам. Каждая грань, каждый угол вычисляется. Никакой магии, только геометрия.

Эта статья про похожий путь. Про то, как задача «оценить качество тестов» прошла путь от экспериментов с нейросетями до детерминированного скрипта. От результатов, которые невозможно воспроизвести и объяснить, к инструменту, который даёт одинаковый ответ при каждом запуске. От магии к геометрии.


Небольшое предупреждение перед тем, как нырнём в детали. В конце статьи я даю ссылки на расширение для VS Code и плагин для IntelliJ IDEA. Это рабочие прототипы, не готовый промышленный инструмент. Они функционируют, ими можно пользоваться, но до состояния «поставил и забыл» ещё далеко.

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

Исходный код пока не в открытом доступе. Но если тема вас зацепила и есть желание участвовать в развитии инструмента, пишите. Готов подключать к работе тех, кому это действительно интересно.

А теперь к делу.


Когда я начал экспериментировать с нейросетями для анализа тестов, всё выглядело многообещающе. Закидываешь код, получаешь оценку. Магия! Но чем дольше работал, тем больше понимал: магия плохо масштабируется.

Нейросеть выдавала разные оценки для одного и того же кода при повторных запусках. Объяснить, почему тест получил 67 баллов, а не 72, было невозможно. Стоимость каждого запроса складывалась в ощутимые суммы. А главное: я не мог гарантировать результат. Сегодня модель считает sleep() антипаттерном, завтра пропустит.

Тогда я сделал шаг назад и задал себе вопрос: а что именно я пытаюсь автоматизировать? Не «умный анализ кода вообще», а конкретные проверки с конкретными критериями. Подсчёт матчеров. Поиск антипаттернов по регулярным выражениям. Определение уровня глубины проверок.

Всё это можно сделать детерминированно. Без нейросетей. Без внешних зависимостей. Один скрипт, который даёт одинаковый результат при каждом запуске.

Так родился EVA-скрипт.

Философия: снять рутину, а не заменить человека

Прежде чем показывать код, важно проговорить принцип.

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

EVA снимает с эксперта рутину. Вместо того чтобы вручную просматривать 200 тестов и считать, сколько из них проверяют только статус-код, скрипт делает это за секунды. Эксперт получает отчёт и может сосредоточиться на том, что требует человеческого суждения: почему тесты такие слабые и как это исправить.

Это разделение труда. Машина делает то, что умеет хорошо: считает, ищет паттерны, агрегирует. Человек делает то, что умеет хорошо: думает, принимает решения, понимает контекст.

Путь к инструменту: от веб-приложения к IDE

Первая версия была веб-приложением. Загружаешь архив с тестами, получаешь отчёт в браузере. Работало, но неудобно. Каждый раз нужно собирать файлы, загружать, ждать. Оторвано от рабочего процесса.

Потом пришло понимание: анализ тестов должен быть там, где пишутся тесты. В IDE. Открыл файл, запустил анализ, увидел результат. Без переключения контекста.

Выбор пал на VS Code. Огромная база пользователей, простая модель расширений, JavaScript/TypeScript под капотом. Так появилось расширение Pe4King (игра слов с почтальонами), в которое EVA-скрипт встроился естественным образом.

Но мир не ограничивается VS Code. Java-разработчики живут в IntelliJ IDEA. Для них я сделал отдельный плагин с той же логикой.

Ядро при этом осталось одним: JavaScript-скрипт, который можно запустить из командной строки, из расширения VS Code, из плагина IDEA или из CI/CD пайплайна. Один код, много точек входа.

Почему JavaScript?

Выбор языка реализации может показаться неочевидным. Инструмент для анализа Java и Python тестов написан на JavaScript. Но у этого решения есть прагматичные основания.

Первое: нативная интеграция с VS Code. Расширения для VS Code пишутся на TypeScript/JavaScript. Скрипт на том же языке встраивается без дополнительных прослоек. Вызов через child_process работает из коробки, результат парсится как JSON без преобразований между языками.

Второе: Node.js есть везде. Любой разработчик, использующий VS Code, npm или современный фронтенд, уже имеет установленный Node.js. Порог входа нулевой. Команда node eva.js работает без предварительной настройки.

Третье: лаконичный синтаксис для работы с текстом. JavaScript исторически силён в обработке строк и регулярных выражений. Regex-литералы читаются естественно. Методы вроде .match(), .test(), .exec() позволяют писать компактный код.

Четвёртое: нулевые внешние зависимости. Весь скрипт использует только стандартную библиотеку Node.js. Никаких npm install, никаких package.json, никаких конфликтов версий. Один файл, полностью автономный.

Пятое: простота кастомизации. Тестировщик, желающий добавить свой паттерн или изменить веса метрик, может сделать это в любом текстовом редакторе. Структура кода намеренно плоская: константы в начале, функции-детекторы в середине, расчёт в конце.

// Добавить свой антипаттерн: одна строка в массиве
const ANTI_PATTERNS = {
  java: [
    // ... существующие паттерны
    { pattern: /System\.out\.println/g, name: 'Console output', penalty: 5 }
  ]
};

Архитектура: как устроен анализ

Скрипт работает как конвейер преобразований. На входе путь к файлам или архиву, на выходе JSON с оценкой и рекомендациями.

Структура основного скрипта
Структура основного скрипта

Каждый этап изолирован. Можно заменить поиск файлов, не трогая логику подсчёта. Можно добавить новый детектор, не меняя форматирование вывода.

Конфигурация: веса и пороги

Веса метрик вынесены в константу в начале скрипта. Это позволяет адаптировать EVA под разные контексты.

const WEIGHTS = {
  oracle: 0.30,      // Сила проверок: главная метрика
  mutation: 0.25,    // Способность ловить ошибки
  negative: 0.20,    // Покрытие ошибочных сценариев
  edge: 0.15,        // Граничные случаи
  structure: 0.10    // Качество кода самого теста
};

Сумма весов равна единице. Итоговый балл представляет собой взвешенную сумму пяти компонентов. Если для вашей команды критичнее негативные сценарии, увеличьте соответствующий вес.

Грейды тоже настраиваемые:

const GRADES = [
  { min: 90, grade: 'S', desc: 'Отлично, эталонное качество' },
  { min: 80, grade: 'A', desc: 'Очень хорошо, незначительные улучшения' },
  { min: 70, grade: 'B', desc: 'Хорошо, есть потенциал' },
  { min: 60, grade: 'C', desc: 'Удовлетворительно, требуется доработка' },
  { min: 50, grade: 'D', desc: 'Слабо, значительная доработка' },
  { min: 0,  grade: 'F', desc: 'Неудовлетворительно, переписать' }
];

function getGrade(score) {
  // Проверяем пороги сверху вниз, возвращаем первый подходящий
  for (const g of GRADES) {
    if (score >= g.min) return g;
  }
  return GRADES[GRADES.length - 1];
}

Порядок элементов в массиве критичен: проверка идёт от большего к меньшему. Грейд F с порогом 0 служит значением по умолчанию.

Детекторы: как ищем паттерны

Каждая метрика реализована как функция-детектор. Разберём несколько примеров.

Поиск copy-paste

Детектор ищет последовательности тестов с похожими именами: testUser1, testUser2, testUser3. Такие последовательности обычно указывают на копирование кода вместо параметризации.

function detectCopyPaste(content, lang) {
  // Выбираем паттерн для поиска имён тестов в зависимости от языка
  const testPattern = lang === 'java'
    ? /@Test\s+public\s+void\s+(\w+)/g
    : /def\s+(test_\w+)/g;

  const testNames = [];
  let match;
  while ((match = testPattern.exec(content)) !== null) {
    testNames.push(match[1]);
  }

  // Группируем имена по префиксу (testUser1, testUser2 дают префикс "testUser")
  const seqPattern = /^(\w+?)(\d+)$/;
  const prefixGroups = {};
  
  for (const name of testNames) {
    const seqMatch = name.match(seqPattern);
    if (seqMatch) {
      const prefix = seqMatch[1];
      if (!prefixGroups[prefix]) prefixGroups[prefix] = [];
      prefixGroups[prefix].push(parseInt(seqMatch[2], 10));
    }
  }

  // Находим самую длинную последовательность
  let maxSequence = 0;
  for (const nums of Object.values(prefixGroups)) {
    if (nums.length > maxSequence) {
      maxSequence = nums.length;
    }
  }

  return {
    detected: maxSequence >= 3,
    sequence: maxSequence,
    penalty: maxSequence >= 5 ? 25 : maxSequence >= 3 ? 15 : 0
  };
}

Штраф прогрессивный: три последовательных теста дают минус 15 баллов, пять и более дают минус 25. Это мотивирует переписывать copy-paste в параметризованные тесты.

Анализ покрытия негативных сценариев

Функция проверяет, какие HTTP-коды ошибок тестируются. Простой подход: ищем упоминания кодов в тексте.

const NEGATIVE_SCENARIOS = [
  { code: '400', name: 'Bad Request' },
  { code: '401', name: 'Unauthorized' },
  { code: '403', name: 'Forbidden' },
  { code: '404', name: 'Not Found' },
  { code: '409', name: 'Conflict' },
  { code: '422', name: 'Validation Error' }
];

function analyzeNegativeCoverage(content) {
  const covered = [];
  
  for (const scenario of NEGATIVE_SCENARIOS) {
    // Простая проверка: есть ли код в тексте?
    if (new RegExp(scenario.code, 'g').test(content)) {
      covered.push(scenario.code);
    }
  }
  
  return { 
    covered: covered.length, 
    total: NEGATIVE_SCENARIOS.length, 
    details: covered 
  };
}

Это упрощённая эвристика. Код 404 может встретиться в комментарии или в URL, а не в assertion. Но на практике корреляция достаточно высокая: если в файле есть «404», скорее всего есть и тест на этот статус.

Генерация рекомендаций

На основе собранных метрик скрипт формирует список конкретных рекомендаций. Каждая рекомендация привязана к определённому порогу.

function generateRecommendations(scores, antiPatterns, copyPaste) {
  const recs = [];

  // Критичные проблемы идут в начало списка
  if (antiPatterns.found.length > 0) {
    const names = antiPatterns.found.map(f => f.name).slice(0, 3).join(', ');
    recs.push(`Устраните антипаттерны: ${names}`);
  }

  // Copy-paste
  if (copyPaste.detected) {
    recs.push(`Рефакторинг copy-paste: ${copyPaste.sequence} последовательных тестов`);
  }

  // Слабое покрытие негативных сценариев
  if (scores.negative < 50) {
    recs.push('Добавьте негативные тесты (4xx коды, невалидные данные)');
  }

  // Мало граничных случаев
  if (scores.edge < 40) {
    recs.push('Добавьте граничные случаи (empty, null, boundary values)');
  }

  return recs;
}

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

Точки расширения

Архитектура позволяет легко добавлять новые проверки. Алгоритм такой:

Первый шаг: создать паттерны в отдельном объекте или добавить в существующий массив.

const NEW_PATTERNS = {
  java: [
    { pattern: /yourRegex/g, name: 'Name', penalty: 10 }
  ],
  python: [
    { pattern: /your_regex/g, name: 'Name', penalty: 10 }
  ]
};

Второй шаг: создать функцию детектора.

function detectNewPattern(content, lang) {
  const patterns = NEW_PATTERNS[lang] || [];
  const found = [];
  let totalPenalty = 0;
  
  for (const p of patterns) {
    const matches = content.match(p.pattern);
    if (matches && matches.length > 0) {
      found.push({ name: p.name, count: matches.length });
      totalPenalty += p.penalty;
    }
  }
  
  return { found, penalty: totalPenalty };
}

Третий шаг: вызвать детектор в main() и добавить штраф в итоговый расчёт.

const newPattern = detectNewPattern(allContent, primaryLang);
totalPenalty += newPattern.penalty;

Три шага, и новая проверка работает.

Ограничения подхода

Статический анализ на основе регулярных выражений имеет свои границы. Скрипт не понимает семантику кода, он ищет паттерны в тексте. Это означает несколько компромиссов.

Первый компромисс касается ложных срабатываний. Код 404 в комментарии будет засчитан как покрытие негативного сценария. Слово «empty» в имени переменной засчитается как граничный случай. На практике это редко искажает общую картину, но для единичных тестов точность может страдать.

Второй компромисс связан с пропусками. Если assertion написан нестандартным способом, паттерн его не найдёт. Кастомные матчеры, обёрнутые в хелперы, могут не распознаваться. Скрипт оптимизирован под типичные паттерны REST Assured и pytest.

Третий компромисс затрагивает контекст. Скрипт не различает активный код и закомментированный. Не понимает условную логику. Не видит наследование классов. Для глубокого анализа нужен полноценный парсер AST, но это значительно усложнило бы инструмент.

Несмотря на эти ограничения, EVA даёт полезную оценку для большинства реальных кодовых баз. Погрешность в 5–10% некритична, когда цель состоит в выявлении системных проблем с качеством тестов.

Режимы использования

EVA может работать в нескольких режимах.

Командная строка (локально)

node eva.js ./tests/
node eva.js tests.zip --json

Самый простой способ. Подходит для разовых проверок и экспериментов.

CI/CD Pipeline

- script: node eva.js ./tests/ --json > eva-report.json
  # Exit code: 0 если score >= 60, иначе 1

Exit code позволяет использовать EVA как quality gate: если оценка ниже порога, сборка падает.

VS Code Extension

Расширение Pe4King интегрирует EVA в ре��актор. Анализ запускается из контекстного меню или по горячей клавише. Результат отображается в панели с подсветкой проблемных мест.

IntelliJ IDEA Plugin

Для Java-разработчиков я сделал отдельный плагин с той же логикой. Интерфейс адаптирован под UX платформы JetBrains.

Ядро при этом остаётся одним. Обновляю логику в одном месте, и все точки входа получают изменения.

Пример вывода

Вот как выглядит результат анализа для набора тестов:

Пример отчета
Пример отчета

Грейд F означает, что тесты существуют, но практически бесполезны. Они создают иллюзию покрытия, но не ловят ошибки. Рекомендации показывают, с чего начать исправление.

Больше, чем оценка: дополнительные возможности

EVA вырос из задачи оценки тестов, но расширение и плагин умеют больше. Раз уж инструмент живёт в IDE и понимает структуру API-тестов, логично добавить смежные функции.

Отправка запросов прямо из редактора. Пишешь тест, хочешь проверить, что endpoint вообще работает. Не нужно переключаться в Postman или curl. Выделяешь URL и параметры, запускаешь команду, видишь ответ в соседней панели. Быстрая проверка гипотезы без выхода из контекста.

Генерация тестов из JSON-ответа. Получил ответ от API, хочешь написать тест. Вставляешь JSON, указываешь глубину проверок, получаешь готовый код теста с assertions. Не идеальный, но рабочий каркас, который можно доработать. Экономит время на рутинном наборе проверок полей.

Генерация тестов из Swagger/OpenAPI. Загружаешь спецификацию, выбираешь endpoints, получаешь набор тестов. Для каждого endpoint создаются позитивный сценарий и базовые негативные: невалидные параметры, отсутствующая авторизация, несуществующий ресурс. Покрытие не полное, но стартовая точка уже есть.

Эти функции не заменяют думающего тестировщика. Они убирают механическую работу: набор boilerplate-кода, переключение между инструментами, копирование структур данных. Тестировщик может сосредоточиться на том, что действительно требует экспертизы: выбор сценариев, проверка граничных случаев, понимание бизнес-логики.

Экраны плагина
Генерация автотестов
Генерация автотестов
загрузка Slack из публичных данных
загрузка Slack из публичных данных
можно создавать описание тестов для импорта в TMS
можно создавать описание тестов для импорта в TMS
выбор разных форматов выходных данных
выбор разных форматов выходных данных

Что дальше

EVA в текущем виде решает конкретную задачу: быстрая оценка качества API-тестов без запуска и без внешних зависимостей. Это не универсальный инструмент для любого кода, и я не пытаюсь его таким сделать.

Следующие шаги, которые я рассматриваю:

Поддержка большего количества фреймворков. Сейчас хорошо работает с REST Assured и pytest. Хочется добавить Karate, Postman Collections, Robot Framework.

Визуализация трендов. Запускать EVA регулярно и смотреть, как меняется качество тестов со временем. Растёт или падает? После каких событий?

Интеграция с code review. Показывать EVA-оценку для изменённых тестов прямо в pull request. Чтобы ревьюер сразу видел, что новые тесты слабые.

Но главное остаётся неизменным: детерминированный анализ, нулевые внешние зависимости, прозрачная логика. И��струмент, который можно понять, настроить и доверять.

Попробовать самому

Расширение для VS Code и плагин для IntelliJ IDEA доступны на GitHub:

Установка занимает минуту. Откройте папку с тестами, запустите анализ, посмотрите на результат. Если оценка ниже ожидаемой, рекомендации покажут, что исправить в первую очередь.

Обратная связь приветствуется. Если нашли паттерн, который EVA не ловит, или антипаттерн, который должен штрафоваться, но не штрафуется, пишите в issues.

Инструмент развивается на основе реальных кейсов.


Это завершающая часть серии про EVA. В первой части я разобрал проблему: почему традиционные метрики не работают. Во второй описал методологию: что и как измерять. В третьей показал инструмент: как автоматизировать оценку.

Методология без инструмента остаётся теорией. Инструмент без методологии остаётся чёрным ящиком. Вместе они дают практическую систему для улучшения качества тестов.