Тестирование плагина

Тестирование ESLint-плагина опирается на проверку трёх основных слоёв: правил (rules), конфигураций (configs) и дополнительных компонентов вроде парсеров, процессоров и утилит. Центральное место занимает проверка правил, поскольку именно они определяют поведение линтера и формируют диагностические сообщения.

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


Тестирование правил через RuleTester

Основной инструмент проверки правил ESLint — RuleTester, встроенный в пакет ESLint. Он позволяет описывать набор корректных и некорректных сценариев и автоматически проверяет поведение правила.

Типовая структура теста:

import { RuleTester } from "eslint";
import rule from "../. ./src/rules/no-foo.js";

const ruleTester = new RuleTester({
  languageOptions: {
    ecmaVersion: 2022,
    sourceType: "module",
  },
});

ruleTester.run("no-foo", rule, {
  valid: [
    "const a = 1;",
    "function test() { return 42; }",
  ],

  invalid: [
    {
      code: "const foo = 1;",
      errors: [{ messageId: "unexpectedFoo" }],
    },
  ],
});

Ключевая идея заключается в строгом разделении входных данных:

  • valid — код, который не должен порождать ошибок
  • invalid — код, который обязан вызвать диагностику

Проверка сообщений и messageId

Современные правила ESLint используют messageId вместо строковых сообщений. Это повышает стабильность тестов и снижает чувствительность к текстовым изменениям.

Пример правила:

export default {
  meta: {
    messages: {
      unexpectedFoo: "Использование foo запрещено",
    },
  },
  create(context) {
    return {
      Identifier(node) {
        if (node.name === "foo") {
          context.report({
            node,
            messageId: "unexpectedFoo",
          });
        }
      },
    };
  },
};

Тест проверяет именно идентификатор сообщения:

invalid: [
  {
    code: "foo();",
    errors: [{ messageId: "unexpectedFoo" }],
  },
]

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


Проверка фиксов (output fixers)

Многие правила ESLint поддерживают автоматическое исправление кода через fix функцию. Тестирование фиксов требует отдельного поля output.

invalid: [
  {
    code: "const foo = 1;",
    output: "const bar = 1;",
    errors: [{ messageId: "renameFoo" }],
  },
]

Если правило возвращает fixer, но output не совпадает с результатом, тест считается проваленным.

Важные нюансы:

  • фикс должен быть полностью детерминированным
  • нельзя использовать асинхронные операции
  • фикс не должен зависеть от состояния вне AST

Тестирование диапазонов и колонок

ESLint позволяет проверять точное расположение ошибок через line и column:

errors: [
  {
    messageId: "unexpectedFoo",
    line: 1,
    column: 7,
  },
]

Это важно для правил, где позиция ошибки влияет на корректность диагностики (например, форматирование или анализ цепочек вызовов).


Тестирование сложных AST-сценариев

Для правил, работающих с синтаксическим деревом, часто используются многострочные примеры:

invalid: [
  {
    code: `
      function test() {
        const foo = 1;
        return foo;
      }
    `,
    errors: [{ messageId: "unexpectedFoo" }],
  },
]

В таких случаях важно контролировать:

  • корректную интерпретацию отступов
  • отсутствие ложных срабатываний на вложенные конструкции
  • работу с блоками и вложенными функциями

Использование парсеров в тестах

Если плагин поддерживает TypeScript или JSX, необходимо указывать соответствующий парсер:

const ruleTester = new RuleTester({
  languageOptions: {
    parser: "@typescript-eslint/parser",
    ecmaVersion: 2022,
  },
});

Без явного указания парсера тесты могут давать ложные результаты или вовсе не запускаться.


Тестирование конфигураций плагина

Помимо правил, ESLint-плагины часто экспортируют набор конфигураций:

export default {
  configs: {
    recommended: {
      rules: {
        "my-plugin/no-foo": "error",
      },
    },
  },
};

Тестирование конфигураций включает:

  • проверку структуры объекта
  • проверку существования правил
  • валидацию совместимости с ESLint schema

Обычно используется запуск ESLint programmatically:

import { ESLint } from "eslint";

const eslint = new ESLint({
  overrideConfigFile: true,
  baseConfig: {
    plugins: ["my-plugin"],
    rules: {
      "my-plugin/no-foo": "error",
    },
  },
});

const results = await eslint.lintText("foo = 1;");

Интеграционное тестирование через ESLint API

Для более сложных сценариев применяется прямой запуск ESLint:

const eslint = new ESLint({
  useEslintrc: false,
  overrideConfig: {
    parserOptions: { ecmaVersion: 2022 },
    plugins: ["my-plugin"],
  },
});

const results = await eslint.lintText("const foo = 1;");

Этот подход позволяет проверять:

  • корректность регистрации плагина
  • взаимодействие нескольких правил
  • поведение в реальной конфигурации проекта

Тестирование процессоров (processors)

Если плагин работает с нестандартными файлами (например, .vue или markdown), тестируется процессор, который преобразует содержимое в линтуемый код.

Типовой тест включает:

  • входной “сырой” текст
  • ожидаемый набор виртуальных файлов
  • проверку сообщений ESLint

Мокирование контекста и утилит

Иногда требуется тестировать утилиты правил отдельно от ESLint runtime:

const context = {
  report: jest.fn(),
  getSourceCode: () => ({
    text: "foo",
  }),
};

Такой подход применяется для:

  • проверки чистой логики
  • ускорения тестов
  • изоляции от ESLint AST API

Табличное тестирование правил

Для больших правил удобно использовать табличный подход:

const cases = [
  { code: "foo", valid: false },
  { code: "bar", valid: true },
];

cases.forEach((test) => {
  // генерация valid/invalid сценариев
});

Однако при использовании RuleTester прямые таблицы часто заменяются декларативными структурами valid/invalid.


Тестирование производительности правил

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

const bigCode = "foo();".repeat(10000);

Контроль:

  • время выполнения lint
  • отсутствие бесконечных циклов
  • стабильность памяти

Тестирование совместимости версий ESLint

Плагины могут зависеть от конкретных версий ESLint API. Поэтому тесты часто разделяют по окружениям:

  • ESLint 7.x
  • ESLint 8.x
  • ESLint 9.x

Основная цель — выявление изменений в AST и RuleTester API.


Организация тестовой структуры проекта

Типичная структура тестов ESLint-плагина:

tests/
  rules/
    no-foo.test.js
    no-bar.test.js
  configs/
    recommended.test.js
  fixtures/
    invalid.js
    valid.js

Использование фикстур особенно полезно при сложных сценариях, где код слишком объёмный для inline-описания.


Проверка устойчивости к синтаксическим ошибкам

Некоторые тесты включают некорректный код, чтобы убедиться, что правило не падает:

invalid: [
  {
    code: "const = ;",
    errors: [],
  },
]

Цель — гарантировать, что правило корректно обрабатывает parse errors и не вызывает исключений внутри create.