This rule raises an issue when the callback passed to describe(), or one of its aliases or variants, is declared async,
or when a describe() callback calls an async helper that registers tests after an await.
Test frameworks like Jest, Mocha, and Cypress run a suiteās callback during the discovery phase, before any test executes, so they
can collect the test cases nested inside it. The framework expects this callback to be a synchronous function and does not await the promise an
async callback returns.
This applies to every suite-defining form whose callback runs during discovery, grouped here by the frameworks that provide it:
describe() and describe.only(): Jest, Mocha, and Cypress.context() and context.only(): Mocha and Cypress.suite() and suite.only(): Mocha.fdescribe(), describe.each(), describe.only.each(), and fdescribe.each(): Jest only.The callback awaits before registering tests. The framework runs the synchronous portion of the callback, registers whatever was
scheduled before the first await, and moves on. Any it() declared after that await is registered too late and
is silently skipped. The reported test counts can look fine even though tests the developer intended to run never execute. Move the awaited work into
a lifecycle hook the framework awaits, or into the individual test bodies, so that every
it() is reached before any await.
describe('user service', async () => { // Noncompliant: an async suite callback is not awaited, so tests after the await are skipped
const config = await loadConfig();
it('returns users', () => { // never registered
expect(listUsers(config)).toEqual(['alice', 'bob']);
});
});
describe('user service', () => { // Compliant: the suite callback is synchronous
let config;
beforeEach(async () => { // async work moved into a hook the framework awaits
config = await loadConfig();
});
it('returns users', () => {
expect(listUsers(config)).toEqual(['alice', 'bob']);
});
});
The callback does not await anything. All tests still register, but the async keyword is misleading and makes the
callback return a promise that the framework silently ignores. Remove async to make the callback synchronous.
describe('user service', async () => { // Noncompliant: the suite callback is async but awaits nothing, so async is misleading
it('returns users', () => {
expect(listUsers()).toEqual(['alice', 'bob']);
});
});
describe('user service', () => { // Compliant: the suite callback is synchronous
it('returns users', () => {
expect(listUsers()).toEqual(['alice', 'bob']);
});
});
A helper called from the describe callback registers tests after an await. The problem is not limited to the describe
callback itself. If the describe callback calls an async helper that registers tests after its own await, those tests are silently
dropped. Awaiting the helper does not help: the framework does not await the describe() callback. Async work is still valid inside a test
callback or lifecycle hook, because those run after discovery instead of defining tests during it.
async function connectAndRegister() {
const db = await connect();
it('inserts a record', () => { // never registered: runs after discovery
db.insert({ id: 1 });
});
}
describe('database', () => {
connectAndRegister(); // Noncompliant: async helper registers tests after an await; they are silently dropped
});
function registerDatabaseTests() {
it('inserts a record', async () => {
const db = await connect(); // Compliant: async work inside a test callback runs after discovery
db.insert({ id: 1 });
});
}
describe('database', () => {
registerDatabaseTests();
});
async function loadAndRegister() {
const config = await loadConfig();
it('uses the loaded config', () => { // never registered: describe's Promise is ignored by the framework
expect(getConfig()).toEqual(config);
});
}
describe('configuration', async () => {
await loadAndRegister(); // Noncompliant: async helper drops tests even when awaited; the framework does not await the describe callback's Promise
});
function loadAndRegister() {
let config;
beforeEach(async () => { // Compliant: async setup moved into a hook the framework awaits
config = await loadConfig();
});
it('uses the loaded config', () => {
expect(getConfig()).toEqual(config);
});
}
describe('configuration', () => {
loadAndRegister();
});
The rule does not raise on the skip variants, such as describe.skip(), context.skip(), suite.skip(),
xdescribe(), describe.skip.each(), and xdescribe.each(). The suite callback still runs during discovery, but because the tests it contains are skipped regardless, an
async callback there cannot cause tests that were meant to run to be silently dropped.
The rule follows async helper calls one hop deep. An async helper that itself delegates to another async function is not inspected beyond that first call.
The rule only checks helpers defined in the same file as the describe call. Async helpers imported from other modules are not
analyzed.
describe(name, fn)