Тестирование ESLint-плагина опирается на проверку трёх основных слоёв: правил (rules), конфигураций (configs) и дополнительных компонентов вроде парсеров, процессоров и утилит. Центральное место занимает проверка правил, поскольку именно они определяют поведение линтера и формируют диагностические сообщения.
Плагин обычно рассматривается как набор независимых модулей, каждый из которых должен быть детерминированным: одинаковый вход всегда приводит к одинаковому результату. Это свойство критично для автоматизированного тестирования.
Основной инструмент проверки правил 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 — код, который обязан вызвать диагностикуСовременные правила 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" }],
},
]
Проверка текста сообщения используется только в исключительных случаях, когда правило генерирует динамические сообщения.
Многие правила ESLint поддерживают автоматическое исправление кода
через fix функцию. Тестирование фиксов требует отдельного
поля output.
invalid: [
{
code: "const foo = 1;",
output: "const bar = 1;",
errors: [{ messageId: "renameFoo" }],
},
]
Если правило возвращает fixer, но output не
совпадает с результатом, тест считается проваленным.
Важные нюансы:
ESLint позволяет проверять точное расположение ошибок через
line и column:
errors: [
{
messageId: "unexpectedFoo",
line: 1,
column: 7,
},
]
Это важно для правил, где позиция ошибки влияет на корректность диагностики (например, форматирование или анализ цепочек вызовов).
Для правил, работающих с синтаксическим деревом, часто используются многострочные примеры:
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 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:
const eslint = new ESLint({
useEslintrc: false,
overrideConfig: {
parserOptions: { ecmaVersion: 2022 },
plugins: ["my-plugin"],
},
});
const results = await eslint.lintText("const foo = 1;");
Этот подход позволяет проверять:
Если плагин работает с нестандартными файлами (например,
.vue или markdown), тестируется процессор, который
преобразует содержимое в линтуемый код.
Типовой тест включает:
Иногда требуется тестировать утилиты правил отдельно от ESLint runtime:
const context = {
report: jest.fn(),
getSourceCode: () => ({
text: "foo",
}),
};
Такой подход применяется для:
Для больших правил удобно использовать табличный подход:
const cases = [
{ code: "foo", valid: false },
{ code: "bar", valid: true },
];
cases.forEach((test) => {
// генерация valid/invalid сценариев
});
Однако при использовании RuleTester прямые таблицы часто заменяются
декларативными структурами valid/invalid.
Для правил, работающих с большими AST, важно учитывать стоимость обхода дерева. Проверка обычно проводится через генерацию больших входных файлов:
const bigCode = "foo();".repeat(10000);
Контроль:
Плагины могут зависеть от конкретных версий ESLint API. Поэтому тесты часто разделяют по окружениям:
Основная цель — выявление изменений в 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.