Test a custom ESLint rule with the built-in RuleTester class. Give it a rule and two lists: valid code that must produce no report, and invalid code with the errors you expect. Assert on messageId, not message text, and add edge cases for every false positive you have seen.
What RuleTester does
RuleTester is a utility that ESLint uses internally for its bundled rules, and plugins can use it too. Per the ESLint tutorial, it needs no third-party testing library, and it works with common runners. The run method throws an error if any scenario fails, and it requires at least one valid and one invalid scenario.
A basic test file
const { RuleTester } = require("eslint");
const rule = require("./no-nonliteral-shell");
const ruleTester = new RuleTester({
languageOptions: { ecmaVersion: 2022, sourceType: "module" }
});
ruleTester.run("no-nonliteral-shell", rule, {
valid: [
'runShell("ls")',
'other(userInput)'
],
invalid: [
{
code: "runShell(userInput)",
errors: [{ messageId: "nonLiteral" }]
}
]
});
Without constructor options, RuleTester uses the ESLint defaults of ecmaVersion latest and sourceType module. Set the options explicitly if your code base needs something else.
What to put in each list
| List | What belongs there | Why |
|---|---|---|
| valid | The safe pattern you want to allow | Proves the rule does not over-report |
| valid | Near misses, such as a different function with a similar name | Catches name matching that is too loose |
| invalid | The exact pattern you set out to catch | Proves the rule works at all |
| invalid | Variants: template literals, concatenation, aliased calls | Shows which variants you cover on purpose |
| valid or invalid | Every real false positive reported by developers | Stops regressions |
Steps to a test suite you can trust
- Write the invalid case first and watch it fail before the rule exists.
- Add the valid case that looks most like it.
- Assert on messageId so rewording a message does not break tests. The ESLint docs also show an assertion option that can require a message or message data, if you want stricter checks.
- Add a test for each bug report. Name the case after the ticket in a comment.
- Run the suite in CI so a rule change cannot merge with failing tests.
Test the options too
If your rule takes options, the schema is mandatory, so test that valid options pass and that a bad option fails loudly. A rule that silently ignores a typo in its config is worse than no rule, because it looks enabled.
What tests cannot tell you
Fixtures show that the rule does what you meant. They do not show how it behaves on your real code base. Before enabling a rule as an error, run it on the full repository in report only mode and read a sample of the results. A rule can pass every unit test and still be noisy on code shaped differently than your fixtures.
Try it yourself
Click through it at your own pace, or open it full screen.
Open a few findings and ask of each one: would my valid list have contained this code, or my invalid list?
Quick check
Should tests assert on message text?
Prefer messageId. Message text changes whenever someone improves the wording, and those edits should not break the suite.
How many test cases is enough?
There is no fixed number. Cover the intended pattern, the closest safe look-alikes, each variant you decided to support, and every false positive you have fixed.
Safeguard's scanning covers dependencies and SBOMs, and its reachability analysis helps show which findings your code can actually call. You can run a free scan on a repository.
Sources
Last reviewed: 19 October 2026.