При разработке пользовательских правил ESLint недостаточно написать логику проверки. Любое правило должно сопровождаться автоматическими тестами, подтверждающими корректность его работы при различных вариантах входного кода. Для этой задачи ESLint предоставляет специальный инструмент — RuleTester.
RuleTester позволяет запускать правило на наборе
тестовых примеров и проверять:
Тестирование правил является обязательной частью разработки ESLint-плагинов и пользовательских наборов правил.
В современных версиях ESLint тестировщик импортируется из пакета ESLint:
const { RuleTester } = require("eslint");
const rule = require("../lib/rules/no-console-log");
Для ECMAScript-модулей:
import { RuleTester } from "eslint";
import rule from "../lib/rules/no-console-log.js";
После импорта создаётся экземпляр тестировщика:
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 2022,
sourceType: "module"
}
});
Параметры конструктора определяют настройки парсинга, которые будут использоваться при выполнении тестов.
Основным методом является run().
Общий синтаксис:
ruleTester.run(
"rule-name",
rule,
{
valid: [],
invalid: []
}
);
Аргументы метода:
Пример:
ruleTester.run(
"no-console-log",
rule,
{
valid: [
"console.error('Ошибка')",
"alert('Hello')"
],
invalid: [
{
code: "console.log('Test')",
errors: [
{
message: "Использование console.log запрещено"
}
]
}
]
}
);
Секция valid содержит примеры, которые не должны
вызывать ошибок.
Простейший вариант:
valid: [
"const x = 10;",
"const name = 'John';"
]
Каждый элемент представляет собой строку с JavaScript-кодом.
Во время выполнения теста ESLint анализирует код и убеждается, что правило не сообщило ни одной ошибки.
Если хотя бы одна ошибка будет найдена, тест завершится неудачей.
Для более сложных сценариев элементы массива valid могут
быть объектами.
Например:
valid: [
{
code: "myFunction()",
options: ["warn"]
}
]
Это позволяет передавать:
Секция invalid предназначена для случаев, когда правило
должно обнаружить нарушение.
Пример:
invalid: [
{
code: "var x = 1;",
errors: [
{
message: "Использование var запрещено"
}
]
}
]
Здесь ожидается, что правило сообщит одну ошибку с указанным текстом.
Если ошибка не будет найдена либо сообщение окажется другим, тест завершится с ошибкой.
Иногда важно проверить только число нарушений.
Пример:
invalid: [
{
code: `
var a = 1;
var b = 2;
`,
errors: 2
}
]
В этом случае RuleTester ожидает ровно две ошибки.
Если правило сообщит одну или три ошибки, тест не пройдёт.
Часто требуется удостовериться, что правило выводит правильный текст сообщения.
Рассмотрим правило:
context.report({
node,
message: "Не используйте var"
});
Тест:
invalid: [
{
code: "var x = 10;",
errors: [
{
message: "Не используйте var"
}
]
}
]
Проверка текста особенно важна для публичных ESLint-плагинов.
Современные правила обычно используют идентификаторы сообщений.
Пример правила:
meta: {
messages: {
avoidVar: "Не используйте var"
}
}
Сообщение:
context.report({
node,
messageId: "avoidVar"
});
Тест:
invalid: [
{
code: "var x = 1;",
errors: [
{
messageId: "avoidVar"
}
]
}
]
Такой подход более устойчив к изменениям текста сообщений.
Сообщения могут содержать шаблоны.
Правило:
messages: {
forbidden: "Переменная '{{name}}' запрещена"
}
Сообщение:
context.report({
node,
messageId: "forbidden",
data: {
name: variableName
}
});
Тест:
invalid: [
{
code: "const temp = 1;",
errors: [
{
message: "Переменная 'temp' запрещена"
}
]
}
]
RuleTester способен проверять тип AST-узла, на который указывает ошибка.
Пример:
invalid: [
{
code: "var x = 1;",
errors: [
{
type: "VariableDeclaration"
}
]
}
]
Проверяется, что нарушение зарегистрировано именно для узла
VariableDeclaration.
Это полезно при сложной логике анализа AST.
Многие правила принимают настройки.
Пример правила:
{
allowFoo: true
}
Тест:
valid: [
{
code: "foo()",
options: [
{
allowFoo: true
}
]
}
]
Проверка ошибки:
invalid: [
{
code: "foo()",
options: [
{
allowFoo: false
}
],
errors: 1
}
]
]
Каждый тестовый сценарий может использовать собственный набор параметров.
Одно правило обычно тестируется сразу в нескольких конфигурациях.
Пример:
ruleTester.run(
"example-rule",
rule,
{
valid: [
{
code: "foo()",
options: [{ allowFoo: true }]
}
],
invalid: [
{
code: "foo()",
options: [{ allowFoo: false }],
errors: 1
}
]
}
);
Такой подход позволяет убедиться, что настройки действительно влияют на поведение правила.
Одной из важнейших возможностей ESLint является автоисправление ошибок.
Если правило содержит функцию fix, её необходимо
тестировать.
Правило:
fix(fixer) {
return fixer.replaceText(node, "let");
}
Тест:
invalid: [
{
code: "var x = 1;",
output: "let x = 1;",
errors: 1
}
]
Поле output содержит ожидаемый результат после
применения исправления.
Иногда правило сообщает об ошибке, но не должно ничего исправлять.
В таком случае используется:
invalid: [
{
code: "problematicCode()",
output: null,
errors: 1
}
]
]
Значение null означает отсутствие автоматического
исправления.
Если правило исправляет несколько участков кода одновременно,
итоговый результат также можно проверить через output.
Пример:
invalid: [
{
code: `
var a = 1;
var b = 2;
`,
output: `
let a = 1;
let b = 2;
`,
errors: 2
}
]
RuleTester выполнит все исправления и сравнит результат с ожидаемым кодом.
Для поддержки различных стандартов JavaScript используются настройки
languageOptions.
Пример для современных возможностей языка:
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: "latest"
}
});
Тестируемый код:
const result = obj?.name;
Без соответствующей настройки парсер может завершиться ошибкой ещё до запуска правила.
Для поддержки конструкции import/export необходимо
указать тип исходного кода.
const ruleTester = new RuleTester({
languageOptions: {
sourceType: "module"
}
});
После этого становятся доступны тесты:
import fs from "fs";
export default {};
Некоторые правила работают с TypeScript или экспериментальными расширениями синтаксиса.
В этом случае указывается другой парсер.
Пример:
{
languageOptions: {
parser: require("@typescript-eslint/parser")
}
}
Тест:
valid: [
{
code: `
interface User {
name: string;
}
`,
languageOptions: {
parser: require("@typescript-eslint/parser")
}
}
]
Каждый элемент массива valid и invalid
запускается независимо.
Например:
invalid: [
{
code: "var a = 1;",
errors: 1
},
{
code: "var b = 2;",
errors: 1
}
]
Ошибка в одном сценарии не влияет на остальные.
Такое поведение позволяет создавать большие наборы тестов без взаимозависимостей.
Типичная структура ESLint-плагина:
eslint-plugin-example/
├── lib/
│ └── rules/
│ └── no-console-log.js
│
└── tests/
└── lib/
└── rules/
└── no-console-log.test.js
Тест обычно импортирует правило:
const rule = require(
"../. ./. ./lib/rules/no-console-log"
);
После чего создаётся экземпляр RuleTester и запускаются проверки.
Правило:
module.exports = {
meta: {
type: "problem",
messages: {
avoidVar: "Использование var запрещено"
}
},
create(context) {
return {
VariableDeclaration(node) {
if (node.kind === "var") {
context.report({
node,
messageId: "avoidVar"
});
}
}
};
}
};
Тест:
const { RuleTester } = require("eslint");
const rule = require("../lib/rules/no-var");
const tester = new RuleTester({
languageOptions: {
ecmaVersion: "latest"
}
});
tester.run(
"no-var",
rule,
{
valid: [
"let x = 1;",
"const y = 2;"
],
invalid: [
{
code: "var z = 3;",
errors: [
{
messageId: "avoidVar",
type: "VariableDeclaration"
}
]
}
]
}
);
errors: 1
При фактических двух сообщениях тест завершится неудачей.
errors: [
{
message: "Ошибка"
}
]
Даже небольшое отличие строки приведёт к провалу теста.
output: "let x = 1;"
Если правило сгенерирует:
let x = 1;
тест будет считаться неуспешным.
Код:
import fs from "fs";
при отсутствии:
sourceType: "module"
не сможет быть разобран парсером.
Для каждого правила желательно проверять:
messageId;Полноценный набор тестов делает пользовательские правила ESLint предсказуемыми, облегчает рефакторинг и гарантирует стабильность поведения при обновлении ESLint и изменении внутренней логики правила.