This rule raises an issue when a test callback contains a conditional return used to bail out of the test instead of calling the test framework’s explicit skip mechanism.

Why is this an issue?

Test frameworks provide a way to mark a test as skipped when one of its prerequisites is not met, for example when an external resource is unavailable or a feature is disabled in the current environment.

Some tests use a plain return statement instead, to leave the test early before running any assertion:

it('reorders columns', async () => {
  if (readOnlyMode) {
    return; // Noncompliant: the test is reported as passing, even though nothing was verified
  }
  await db.reorderColumns();
  expect(db.columns).toEqual(['a', 'b']);
});

This looks harmless, but every one of the frameworks covered by this rule reports such a test as passed, not skipped, because it completed without an unhandled error or a failed assertion. The test report gives no indication that the assertions never ran, which hides the fact that this part of the code is not actually being verified.

This rule raises an issue when a test callback’s body’s very first statement is a conditional return with no value (an if with no else, whose branch only exits the function) that is followed by more code. A guard preceded by other statements is not flagged. It does not raise when the return is the only statement reached in that path, since in that case nothing is being skipped over, nor when the return carries a value, since a test callback returning a value or a promise is typically an unrelated, legitimate pattern.

An unconditional return followed by other statements is dead code and is already covered by {rule:javascript:S1763} - All code should be reachable.

This rule covers Mocha, Vitest, Playwright, node:test, and Bun. AVA and QUnit files are detected and excluded from raising: both already fail a test by default when it completes without running an assertion, so the misleading "passed" outcome this rule targets does not occur there. Jest and Jasmine files are detected and excluded from raising too: unlike Bun, Jest has neither an in-body skip call nor a declarative skipIf-style alternative, so there is no fix to suggest; Jasmine is not covered by this version of the rule even though it has its own in-body skip mechanism (pending()).

How to fix it in Mocha

Call this.skip() instead of returning early. Because this.skip() relies on the Mocha test context, the test callback must be declared with the function keyword instead of an arrow function.

Code examples

Noncompliant code example

it('reorders columns', function () {
  if (readOnlyMode) {
    return; // Noncompliant
  }
  db.reorderColumns();
  expect(db.columns).toEqual(['a', 'b']);
});

Compliant solution

it('reorders columns', function () {
  if (readOnlyMode) {
    this.skip();
  }
  db.reorderColumns();
  expect(db.columns).toEqual(['a', 'b']);
});

How to fix it in Vitest

Call skip() on the test context instead of returning early. The test callback receives the context as its first argument.

Code examples

Noncompliant code example

import { it, expect } from 'vitest';

it('reorders columns', async () => {
  if (readOnlyMode) {
    return; // Noncompliant
  }
  await db.reorderColumns();
  expect(db.columns).toEqual(['a', 'b']);
});

Compliant solution

import { it, expect } from 'vitest';

it('reorders columns', async ({ skip }) => {
  if (readOnlyMode) {
    skip();
  }
  await db.reorderColumns();
  expect(db.columns).toEqual(['a', 'b']);
});

When the condition is already known before the test runs, prefer the more declarative it.skipIf(readOnlyMode)('reorders columns', async () ⇒ { /* …​ */ }).

How to fix it in Playwright

Call test.skip() with the condition and, optionally, a reason, instead of returning early.

Code examples

Noncompliant code example

import { test, expect } from '@playwright/test';

test('reorders columns', async ({ page }) => {
  if (readOnlyMode) {
    return; // Noncompliant
  }
  await page.click('#reorder');
  await expect(page.locator('.column')).toHaveText(['a', 'b']);
});

Compliant solution

import { test, expect } from '@playwright/test';

test('reorders columns', async ({ page }) => {
  test.skip(readOnlyMode, 'Reordering is disabled in read-only mode');
  await page.click('#reorder');
  await expect(page.locator('.column')).toHaveText(['a', 'b']);
});

How to fix it in Node.js

Call skip() on the test context before returning.

Code examples

Noncompliant code example

import test from 'node:test';
import assert from 'node:assert/strict';

test('reorders columns', t => {
  if (readOnlyMode) {
    return; // Noncompliant
  }
  db.reorderColumns();
  assert.deepEqual(db.columns, ['a', 'b']);
});

Compliant solution

import test from 'node:test';
import assert from 'node:assert/strict';

test('reorders columns', t => {
  if (readOnlyMode) {
    t.skip('Reordering is disabled in read-only mode');
    return;
  }
  db.reorderColumns();
  assert.deepEqual(db.columns, ['a', 'b']);
});

When the condition is already known before the test runs, prefer the more declarative test('reorders columns', { skip: readOnlyMode }, t ⇒ { /* …​ */ }).

How to fix it in Bun

Bun’s test runner has no way to skip a test from inside a running test. Move the condition to the test declaration instead of returning early. This is only possible when the condition does not depend on state that is only available once the test callback starts running; when it does, no framework mechanism can satisfy this fix, and this is an accepted limitation for Bun.

Code examples

Noncompliant code example

import { test, expect } from 'bun:test';

test('reorders columns', () => {
  if (readOnlyMode) {
    return; // Noncompliant
  }
  db.reorderColumns();
  expect(db.columns).toEqual(['a', 'b']);
});

Compliant solution

import { test, expect } from 'bun:test';

test.skipIf(readOnlyMode)('reorders columns', () => {
  db.reorderColumns();
  expect(db.columns).toEqual(['a', 'b']);
});

Resources

Documentation