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

При разработке пользовательских правил ESLint недостаточно написать логику проверки. Любое правило должно сопровождаться автоматическими тестами, подтверждающими корректность его работы при различных вариантах входного кода. Для этой задачи ESLint предоставляет специальный инструмент — RuleTester.

RuleTester позволяет запускать правило на наборе тестовых примеров и проверять:

  • обнаруживаются ли нарушения;
  • создаются ли ожидаемые сообщения об ошибках;
  • корректно ли работают автоматические исправления;
  • правильно ли обрабатываются различные варианты синтаксиса;
  • не возникают ли ложные срабатывания.

Тестирование правил является обязательной частью разработки ESLint-плагинов и пользовательских наборов правил.


Подключение RuleTester

В современных версиях 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: []
    }
);

Аргументы метода:

  1. Имя тестируемого правила.
  2. Объект правила.
  3. Набор тестовых сценариев.

Пример:

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-плагинов.


Проверка messageId

Современные правила обычно используют идентификаторы сообщений.

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

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 выполнит все исправления и сравнит результат с ожидаемым кодом.


Проверка синтаксических возможностей ECMAScript

Для поддержки различных стандартов 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;
  • поведение на различных AST-конструкциях.

Полноценный набор тестов делает пользовательские правила ESLint предсказуемыми, облегчает рефакторинг и гарантирует стабильность поведения при обновлении ESLint и изменении внутренней логики правила.