Custom ESLint rules #4/5: how to test custom rules

Because tests are a must, right?

No pineapple pizza must be allowed in our code. That is why, in the previous bite, we created a custom ESLint plugin with a dedicated rule to forbid it. It's now time to test it!

Create ESLint plugin unit tests

Install eslint as devDependency and create the test file and test entry point file.

yummy-pizza $ npm i eslint -D -w eslint-plugin-pizza
yummy-pizza $ touch ./packages/eslint-plugin-pizza/rules/no-pineapple-pizza.test.js ./packages/eslint-plugin-pizza/pizza-rules-test-entrypoint.js

Your layout should look like this:

yummy-pizza/
├─ packages/
│  ├─ eslint-plugin-pizza/
│  │  ├─ rules/
│  │  │  ├─ no-pineapple-pizza.js
│  │  │  ├─ no-pineapple-pizza.test.js
│  │  ├─ pizza-rules-test-entrypoint.js
│  │  ├─ package.json
│  ├─ app/
│  │  ├─ src/
│  │  ├─ package.json
├─ package.json
  • no-pineapple-pizza.test.js is our test file

  • pizza-rules-test-entrypoint.js is the file that will be executed by npm t command and that will run our tests

Set up test script in ESLint plugin's package.json to call our entry point.

// yummy-pizza/eslint-plugin-pizza/package.json

{
  "name": "eslint-plugin-pizza",
  "version": "1.0.0",
  "description": "Custom ESLint plugin for pizza",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "test": "node ./pizza-rules-test-entrypoint.js"
  },
  "author": "Gabriele Buffolino",
  "license": "ISC",
  "devDependencies": {
    "eslint": "^8.53.0"
  }
}

Our test entry point file will just import our test files one by one and that's it! It's job is done!

// yummy-pizza/eslint-plugin-pizza/pizza-rules-test-entrypoint.js

import "./rules/no-pineapple-pizza.test.js";

Now we need to define our rule test. Be sure to refer to official docs, but the main gotchas are:

  • you need a RuleTester object

  • you need to invoke its run method passing in

    • the rule name

    • the rule code that should be tested

    • an object containing what code must be considered valid and what invalid

    • for fixable rules, we need to provide also how we expect the invalid code to be fixed


// yummy-pizza/packages/eslint-plugin-pizza/rules/no-pineapple-pizza.test.js

import rule from "./no-pineapple-pizza.js";
import { RuleTester } from "eslint";

const ruleTester = new RuleTester({
  // Must use at least ecmaVersion 2015 because
  // that's when `const` was introduced.
  parserOptions: { ecmaVersion: 2015 },
});

ruleTester.run(
  "no-pineapple-pizza", // rule name
  rule, // rule code
  {
    valid: [
      {
        // valid code
        code: "const yummyPizza = eatPizzaMargherita();",
      },
    ],
    // 'invalid' checks cases that should not pass
    invalid: [
      {
        // code that should be marked as error
        code: "const yummyPizza = eatPineapplePizza();",
        // what should we expect as fix
        output: "const yummyPizza = eatPizzaMargherita();",
        // one error should be detected
        errors: 1,
      },
    ],
  }
);
// share our joy for success
console.log(`[SUCCESS] no-pineapple-pizza`);

Final step: define test script in the root package.json. Using --workspaces and --if-present combined, we will run all packages test scripts and won't error if not present.

// yummy-pizza/package.json

{
  "name": "yummy-pizza",
  "version": "1.0.0",
  "description": "Sample of custom ESLint plugin definition",
  "scripts": {
    "test": "npm run test --workspaces --if-present"
  },
  "author": "Gabriele Buffolino",
  "license": "ISC",
  "workspaces": [
    "packages/*"
  ]
}

Run npm t (short for npm run test) and...

yummy-pizza $ npm t

> yummy-pizza@1.0.0 test
> npm t --workspaces --if-present


> eslint-plugin-pizza@1.0.0 test
> node ./pizza-rules-test-entrypoint.js

[SUCCESS] no-pineapple-pizza

...it works!

In the next bite, we will have a look at how to install our plugin and use it across our application.

References