@typescript-eslint/rule-tester
A utility for testing ESLint rules
This is a fork of ESLint's built-in RuleTester to provide some better types and additional features for testing TypeScript rules.
Usage
For non-type-aware rules you can test them as follows:
import { RuleTester } from '@typescript-eslint/rule-tester';
import rule from '../src/rules/my-rule.ts';
const ruleTester = new RuleTester();
ruleTester.run('my-rule', rule, {
  valid: [
    // valid tests can be a raw string,
    'const x = 1;',
    // or they can be an object
    {
      code: 'const y = 2;',
      options: [{ ruleOption: true }],
    },
    // you can enable JSX parsing by passing parserOptions.ecmaFeatures.jsx = true
    {
      code: 'const z = <div />;',
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
      },
    },
  ],
  invalid: [
    // invalid tests must always be an object
    {
      code: 'const a = 1;',
      // invalid tests must always specify the expected errors
      errors: [
        {
          messageId: 'ruleMessage',
          // If applicable - it's recommended that you also assert the data in
          // addition to the messageId so that you can ensure the correct message
          // is generated
          data: {
            placeholder1: 'a',
          },
        },
      ],
    },
    // fixers can be tested using the output parameter
    {
      code: 'const b = 1;',
      output: 'const c = 1;',
      errors: [
        /* ... */
      ],
    },
    // passing `output = null` will enforce the code is NOT changed
    {
      code: 'const c = 1;',
      output: null,
      errors: [
        /* ... */
      ],
    },
    // suggestions can be tested via errors
    {
      code: 'const d = 1;',
      output: null,
      errors: [
        {
          messageId: 'suggestionError',
          suggestions: [
            {
              messageId: 'suggestionOne',
              output: 'const e = 1;',
            },
          ],
        },
      ],
    },
    // passing `suggestions = null` will enforce there are NO suggestions
    {
      code: 'const d = 1;',
      output: null,
      errors: [
        {
          messageId: 'noSuggestionError',
          suggestions: null,
        },
      ],
    },
  ],
});
Type-Aware Testing
Type-aware rules can be tested in almost exactly the same way, except you need to create some files on disk.
We require files on disk due to a limitation with TypeScript in that it requires physical files on disk to initialize the project.
We suggest creating a fixture folder nearby that contains three files:
file.ts- this should be an empty file.react.tsx- this should be an empty file.tsconfig.json- this should be the config to use for your test, for example:{
"compilerOptions": {
"strict": true
},
"include": ["file.ts", "react.tsx"]
}
It's important to note that both file.ts and react.tsx must both be empty files!
The rule tester will automatically use the string content from your tests - the empty files are just there for initialization.
You can then test your rule by providing the type-aware config:
const ruleTester = new RuleTester({
  parserOptions: {
    tsconfigRootDir: './path/to/your/folder/fixture',
    project: './tsconfig.json',
  },
});
With that config the parser will automatically run in type-aware mode and you can write tests just like before.
Test Dependency Constraints
Sometimes it's desirable to test your rule against multiple versions of a dependency to ensure backwards and forwards compatibility. With backwards-compatibility testing there comes a complication in that some tests may not be compatible with an older version of a dependency. For example - if you're testing against an older version of TypeScript, certain features might cause a parser error!
import type { RangeOptions } from 'semver';
export interface SemverVersionConstraint {
  readonly range: string;
  readonly options?: RangeOptions | boolean;
}
export type AtLeastVersionConstraint =
  | `${number}.${number}.${number}-${string}`
  | `${number}.${number}.${number}`
  | `${number}.${number}`
  | `${number}`;
export type VersionConstraint =
  | AtLeastVersionConstraint
  | SemverVersionConstraint;
/**
 * Passing a string for the value is shorthand for a '>=' constraint
 */
export type DependencyConstraint = Readonly<Record<string, VersionConstraint>>;
The RuleTester allows you to apply dependency constraints at either an individual test or constructor level.
const ruleTester = new RuleTester({
  dependencyConstraints: {
    // none of the tests will run unless `my-dependency` matches the semver range `>=1.2.3`
    'my-dependency': '1.2.3',
    // you can also provide granular semver ranges
    'my-granular-dep': {
      // none of the tests will run unless `my-granular-dep` matches the semver range `~3.2.1`
      range: '~3.2.1',
    },
  },
});
ruleTester.run('my-rule', rule, {
  valid: [
    {
      code: 'const y = 2;',
      dependencyConstraints: {
        // this test won't run unless BOTH dependencies match the given ranges
        first: '1.2.3',
        second: '3.2.1',
      },
    },
  ],
  invalid: [
    /* ... */
  ],
});
All dependencies provided in the dependencyConstraints object must match their given ranges in order for a test to not be skipped.
With Specific Frameworks
ESLint's RuleTester relies on some global hooks for tests.
If they aren't available globally, your tests will fail with an error like:
Error: Missing definition for `afterAll` - you must set one using `RuleTester.afterAll` or there must be one defined globally as `afterAll`.
Be sure to set RuleTester's static properties before calling new RuleTester(...) for the first time.
Mocha
Consider setting up RuleTester's static properties in a mochaGlobalSetup fixture:
import * as mocha from 'mocha';
import { RuleTester } from '@typescript-eslint/rule-tester';
RuleTester.afterAll = mocha.after;
Vitest
Consider setting up RuleTester's static properties in a globalSetup script:
import * as vitest from 'vitest';
import { RuleTester } from '@typescript-eslint/rule-tester';
RuleTester.afterAll = vitest.afterAll;
// If you are not using vitest with globals: true (https://vitest.dev/config/#globals):
RuleTester.it = vitest.it;
RuleTester.itOnly = vitest.it.only;
RuleTester.describe = vitest.describe;
Options
RuleTester constructor options
import type {
  ClassicConfig,
  ParserOptions,
} from '@typescript-eslint/utils/ts-eslint';
import type { DependencyConstraint } from './DependencyConstraint';
export interface RuleTesterConfig extends ClassicConfig.Config {
  /**
   * The default parser to use for tests.
   * @default '@typescript-eslint/parser'
   */
  readonly parser: string;
  /**
   * The default parser options to use for tests.
   */
  readonly parserOptions?: Readonly<ParserOptions>;
  /**
   * Constraints that must pass in the current environment for any tests to run.
   */
  readonly dependencyConstraints?: DependencyConstraint;
  /**
   * The default filenames to use for type-aware tests.
   * @default { ts: 'file.ts', tsx: 'react.tsx' }
   */
  readonly defaultFilenames?: Readonly<{
    ts: string;
    tsx: string;
  }>;
}
Valid test case options
import type {
  Linter,
  ParserOptions,
  SharedConfigurationSettings,
} from '@typescript-eslint/utils/ts-eslint';
import type { DependencyConstraint } from './DependencyConstraint';
export interface ValidTestCase<TOptions extends Readonly<unknown[]>> {
  /**
   * Name for the test case.
   */
  readonly name?: string;
  /**
   * Code for the test case.
   */
  readonly code: string;
  /**
   * Environments for the test case.
   */
  readonly env?: Readonly<Linter.EnvironmentConfig>;
  /**
   * The fake filename for the test case. Useful for rules that make assertion about filenames.
   */
  readonly filename?: string;
  /**
   * The additional global variables.
   */
  readonly globals?: Readonly<Linter.GlobalsConfig>;
  /**
   * Options for the test case.
   */
  readonly options?: Readonly<TOptions>;
  /**
   * The absolute path for the parser.
   */
  readonly parser?: string;
  /**
   * Options for the parser.
   */
  readonly parserOptions?: Readonly<ParserOptions>;
  /**
   * Settings for the test case.
   */
  readonly settings?: Readonly<SharedConfigurationSettings>;
  /**
   * Run this case exclusively for debugging in supported test frameworks.
   */
  readonly only?: boolean;
  /**
   * Skip this case in supported test frameworks.
   */
  readonly skip?: boolean;
  /**
   * Constraints that must pass in the current environment for the test to run
   */
  readonly dependencyConstraints?: DependencyConstraint;
}
Invalid test case options
import type { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils';
import type { ReportDescriptorMessageData } from '@typescript-eslint/utils/ts-eslint';
import type { DependencyConstraint } from './DependencyConstraint';
import type { ValidTestCase } from './ValidTestCase';
export interface SuggestionOutput<TMessageIds extends string> {
  /**
   * Reported message ID.
   */
  readonly messageId: TMessageIds;
  /**
   * The data used to fill the message template.
   */
  readonly data?: ReportDescriptorMessageData;
  /**
   * NOTE: Suggestions will be applied as a stand-alone change, without triggering multi-pass fixes.
   * Each individual error has its own suggestion, so you have to show the correct, _isolated_ output for each suggestion.
   */
  readonly output: string;
  // we disallow this because it's much better to use messageIds for reusable errors that are easily testable
  // readonly desc?: string;
}
export interface TestCaseError<TMessageIds extends string> {
  /**
   * The 1-based column number of the reported start location.
   */
  readonly column?: number;
  /**
   * The data used to fill the message template.
   */
  readonly data?: ReportDescriptorMessageData;
  /**
   * The 1-based column number of the reported end location.
   */
  readonly endColumn?: number;
  /**
   * The 1-based line number of the reported end location.
   */
  readonly endLine?: number;
  /**
   * The 1-based line number of the reported start location.
   */
  readonly line?: number;
  /**
   * Reported message ID.
   */
  readonly messageId: TMessageIds;
  /**
   * Reported suggestions.
   */
  readonly suggestions?: readonly SuggestionOutput<TMessageIds>[] | null;
  /**
   * The type of the reported AST node.
   */
  readonly type?: AST_NODE_TYPES | AST_TOKEN_TYPES;
  // we disallow this because it's much better to use messageIds for reusable errors that are easily testable
  // readonly message?: string | RegExp;
}
export interface InvalidTestCase<
  TMessageIds extends string,
  TOptions extends Readonly<unknown[]>,
> extends ValidTestCase<TOptions> {
  /**
   * Expected errors.
   */
  readonly errors: readonly TestCaseError<TMessageIds>[];
  /**
   * The expected code after autofixes are applied. If set to `null`, the test runner will assert that no autofix is suggested.
   */
  readonly output?: string | null;
  /**
   * Constraints that must pass in the current environment for the test to run
   */
  readonly dependencyConstraints?: DependencyConstraint;
}