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.
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()).
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.
it('reorders columns', function () {
if (readOnlyMode) {
return; // Noncompliant
}
db.reorderColumns();
expect(db.columns).toEqual(['a', 'b']);
});
it('reorders columns', function () {
if (readOnlyMode) {
this.skip();
}
db.reorderColumns();
expect(db.columns).toEqual(['a', 'b']);
});
Call skip() on the test context instead of returning early. The test callback receives the context as its first argument.
import { it, expect } from 'vitest';
it('reorders columns', async () => {
if (readOnlyMode) {
return; // Noncompliant
}
await db.reorderColumns();
expect(db.columns).toEqual(['a', 'b']);
});
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 () ⇒
{ /* … */ }).
Call test.skip() with the condition and, optionally, a reason, instead of returning early.
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']);
});
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']);
});
Call skip() on the test context before returning.
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']);
});
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 ⇒ {
/* … */ }).
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.
import { test, expect } from 'bun:test';
test('reorders columns', () => {
if (readOnlyMode) {
return; // Noncompliant
}
db.reorderColumns();
expect(db.columns).toEqual(['a', 'b']);
});
import { test, expect } from 'bun:test';
test.skipIf(readOnlyMode)('reorders columns', () => {
db.reorderColumns();
expect(db.columns).toEqual(['a', 'b']);
});