jestjs/jest · error · Error

<spyOn> : ${methodName} has already been spied upon Usage: s

Error message

<spyOn> : ${methodName} has already been spied upon
Usage: spyOn(<object>, <methodName>)

What it means

Thrown by SpyRegistry.spyOn when the target method is already a spy and respy is not enabled (spyRegistry.ts:107). SpyRegistry only allows re-spying when allowRespy('true') has been called; otherwise a second spyOn on the same method would clobber the original reference and break restoration.

Source

Thrown at packages/jest-jasmine2/src/jasmine/spyRegistry.ts:107

          getErrorMsg(
            `could not find an object to spy upon for ${methodName}()`,
          ),
        );
      }

      if (methodName === void 0) {
        throw new Error(getErrorMsg('No method name supplied'));
      }

      if (obj[methodName] === void 0) {
        throw new Error(getErrorMsg(`${methodName}() method does not exist`));
      }

      if (obj[methodName] && isSpy(obj[methodName])) {
        if (this.respy) {
          return obj[methodName];
        } else {
          throw new Error(
            getErrorMsg(`${methodName} has already been spied upon`),
          );
        }
      }

      let descriptor;
      try {
        descriptor = Object.getOwnPropertyDescriptor(obj, methodName);
      } catch {
        // IE 8 doesn't support `definePropery` on non-DOM nodes
      }

      if (descriptor && !(descriptor.writable || descriptor.set)) {
        throw new Error(
          getErrorMsg(
            `${methodName} is not declared writable or has no setter`,
          ),
        );

View on GitHub (pinned to 8e6d128e4a)

Solutions

  1. Call jest.restoreAllMocks() in an afterEach to clear spies between tests, or jest.clearAllMocks() if you only need call data reset.
  2. Consolidate the spy into a single location (one beforeEach) and remove duplicates.
  3. If you genuinely need re-spying, opt in via jasmine.getEnv().allowRespy(true) (advanced, can mask real bugs).
  4. Use jest.spyOn within beforeEach with restoreMocks:true in config so each test starts clean.

Example fix

// before
beforeEach(() => { spyOn(db, 'query'); });
it('also spies', () => { spyOn(db, 'query'); }); // throws
// after
beforeEach(() => { jest.spyOn(db, 'query'); });
afterEach(() => jest.restoreAllMocks());
Defensive patterns

Strategy: validation

Validate before calling

afterEach(() => jest.restoreAllMocks());
// or check before re-spying
if (jest.isMockFunction(obj.method)) {
  (obj.method as jest.Mock).mockRestore();
}
spyOn(obj, 'method');

Type guard

import {Spy} from 'jest-mock';
const isJestSpy = (v: unknown): v is jest.SpyInstance =>
  v != null && typeof (v as any)._isMockFunction === 'boolean' && (v as any)._isMockFunction;

Prevention

When it happens

Trigger: spyOn(obj, 'm') called twice in the same file, two beforeEach blocks that both spy on the same method, a custom matcher that internally spies on a method already spied by the test, or a helper that wraps spyOn without checking.

Common situations: Shared beforeEach spies combined with per-test spies; refactoring that moved a spy into a utility imported by multiple specs; multiple test files that share global setup; auto-generated test scaffolding that double-applies spies.

Related errors


AI-assisted analysis of jestjs/jest@8e6d128e4a (2026-08-10). Data as JSON: /api/errors/f4f85881f15d13f6. Report an issue: GitHub.