jestjs/jest · error · Error

Spies must be created in a before function or a spec

Error message

Spies must be created in a before function or a spec

What it means

Jasmine's spy registry stores created spies against the currently-executing runnable (a spec or a before/after). The currentSpies() provider in Env.ts:361-369 throws when currentRunnable() is null — meaning spyOn was called at the top level of the file, outside any describe/beforeEach/it. Spies must be tied to a runnable so they can be torn down after it completes.

Source

Thrown at packages/jest-jasmine2/src/jasmine/Env.ts:364

        }
      };

      this.addReporter = function (reporterToAdd) {
        reporter.addReporter(reporterToAdd);
      };

      this.provideFallbackReporter = function (reporterToAdd) {
        reporter.provideFallbackReporter(reporterToAdd);
      };

      this.clearReporters = function () {
        reporter.clearReporters();
      };

      const spyRegistry = new j$.SpyRegistry({
        currentSpies() {
          if (!currentRunnable()) {
            throw new Error(
              'Spies must be created in a before function or a spec',
            );
          }
          return runnableResources[currentRunnable().id].spies;
        },
      });

      this.allowRespy = function (allow) {
        spyRegistry.allowRespy(allow);
      };

      this.spyOn = function (...args) {
        return spyRegistry.spyOn.apply(spyRegistry, args);
      };

      const suiteFactory = function (description: Circus.TestNameLike) {
        const suite = new j$.Suite({
          id: getNextSuiteId(),

View on GitHub (pinned to f49721c78e)

Solutions

  1. Move the spyOn call inside beforeEach/beforeAll or inside the it() body where a runnable is active.
  2. If the spy is needed across setup, define it inside beforeAll and reference the returned spy handle in tests.
  3. Avoid module-level side effects that call spyOn during import.

Example fix

// before — spy created at top level (no runnable)
const spy = jest.spyOn(math, 'add');
describe('math', () => { it('adds', () => { ... }); });

// after — spy created inside a runnable
describe('math', () => {
  let spy;
  beforeEach(() => { spy = jest.spyOn(math, 'add'); });
  afterEach(() => { spy.mockRestore(); });
  it('adds', () => { ... });
});
Defensive patterns

Strategy: validation

Validate before calling

// Static check: ensure spyOn/jest.spyOn calls live inside a runnable body.
// Simple heuristic: the call's nearest enclosing function is it/test/before*/after*.
// (Enforce via an ESLint custom rule or a pre-test scan.)
function spyNotInRunnable(source: string): boolean {
  // returns true if a jest.spyOn/spyOn call appears at describe-body top level
  return /describe\([^)]*\)\s*=>\s*{[^}]*spyOn/.test(source);
}

Prevention

When it happens

Trigger: Calling `jest.spyOn(...)` (or jasmine spyOn) directly in the module body of a test file or in a describe block's synchronous body (not inside a before* or it). currentRunnable() returns null because no spec or before/after is executing at that moment, so the provider at Env.ts:362 throws.

Common situations: Refactoring a test and moving a spyOn out of beforeEach to the top level; calling spyOn during module evaluation (e.g. in a helper imported for side effects); forgetting to wrap setup in beforeEach; spy creation triggered by an import statement.

Related errors


AI-assisted analysis of jestjs/jest@f49721c78e (2026-08-03). Data as JSON: /data/errors/0d031b7d39e6dda1.json. Report an issue: GitHub.