close

require-hook

Unreleased

Configuration

rslint.config.ts
import { defineConfig, rstestPlugin } from '@rslint/core';

export default defineConfig([
  rstestPlugin.configs.recommended,
  {
    rules: {
      'rstest/require-hook': 'error',
    },
  },
]);

Rule Details

Requires setup and teardown code to be written inside a lifecycle hook rather than directly in a test file's module scope or in a describe callback.

Rstest collects a test file by executing it. Module-level statements and every describe callback run during that collection pass, strictly before any beforeAll or beforeEach runs, and they run even for a suite that is skipped — describe.skip still executes its callback while the file is collected. Setup written there therefore happens whether or not the tests that need it run, happens in file order rather than in the order the suites declare, and has no matching teardown point, since afterAll cannot undo work done before the run began.

Moving the work into beforeAll, beforeEach, afterEach, or afterAll ties it to the tests that need it and gives it a place to be reverted.

The rule reports two kinds of statement at the top level of a file or of a describe callback:

  • a function call that is not part of Rstest's own API;
  • a let or var declaration with an initializer other than null or undefined.

onTestFinished and onTestFailed are reported with a different message. They register work against the test that is currently running, so Rstest rejects either call while the file is collected with onTestFinished() can only be called inside a test. Moving such a call into a test callback is the repair; moving it into a hook is not.

A const declaration is left alone, as are Rstest's own calls: test and suite registrations, lifecycle hooks, expect chains, and calls on the rs / rstest utilities object, including through a renamed import, a namespace import, or import.meta.rstest. A describe callback is only inspected when it is written at the call site; a callback passed by name is not, because the same function may be registered more than once.

Incorrect

import { describe, it, expect } from '@rstest/core';

initializeCityDatabase();

describe('cities', () => {
  let consoleWarnSpy = rs.spyOn(console, 'warn');

  it('has Vienna', () => {
    expect(isCity('Vienna')).toBe(true);
  });
});

Correct

import { beforeEach, describe, it, expect } from '@rstest/core';

beforeEach(() => {
  initializeCityDatabase();
});

describe('cities', () => {
  let consoleWarnSpy;

  beforeEach(() => {
    consoleWarnSpy = rs.spyOn(console, 'warn');
  });

  it('has Vienna', () => {
    expect(isCity('Vienna')).toBe(true);
  });
});

Options

{
  "rstest/require-hook": [
    "error",
    {
      "allowedFunctionCalls": ["enableAutoDestroy"]
    }
  ]
}
OptionTypeDefaultDescription
allowedFunctionCallsstring[][]Names of function calls that may stand outside a hook. A name is matched against the whole callee chain, so helper.setup allows helper.setup() but not setup().