jestjs/jest · error · Error

has already been spied upon

Error message

${propertyName} has already been spied upon

What it means

Thrown by SpyRegistry._spyOnProperty when the property accessor is already a spy and respy is not enabled (spyRegistry.ts:191-198). Same protection as the method variant: a second property-spy would lose the reference to the original getter/setter and break restoration.

Solutions

  1. Restore between tests: afterEach(() => jest.restoreAllMocks()).
  2. Consolidate the property spy into a single beforeEach and remove duplicates.
  3. Opt into respy only if intentional: jasmine.getEnv().allowRespy(true).
  4. Set restoreMocks:true in the Jest config to reset spies automatically.

Example fix

// before
beforeEach(() => spyOn(config, 'url', 'get'));
it('spies again', () => spyOn(config, 'url', 'get')); // throws
// after
beforeEach(() => jest.spyOn(config, 'url', 'get'));
afterEach(() => jest.restoreAllMocks());
Defensive patterns

Strategy: validation

Validate before calling

afterEach(() => jest.restoreAllMocks());
// before re-spying, restore
if (obj[prop] && (obj[prop] as any).and instanceof Object) {
  (obj[prop] as any).and.restore?.();
}
spyOn(obj, prop, 'get');

Type guard

const isPropertySpy = (v: unknown): boolean =>
  v != null && typeof (v as any).and === 'object';

Prevention

When it happens

Trigger: spyOn(obj, 'p', 'get') called twice in one test, two beforeEach hooks that both spy on the same getter, or a custom utility that re-applies a property spy.

Common situations: Shared setup spies plus per-test spies on getters; refactoring that moved the property spy into a shared helper imported by many specs; multiple files with global setup; mocking a config getter both globally and locally.

Related errors


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

Appendix: source

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

      if (!descriptor.configurable) {
        throw new Error(
          getErrorMsg(`${propertyName} is not declared configurable`),
        );
      }

      if (!descriptor[accessType]) {
        throw new Error(
          getErrorMsg(
            `Property ${propertyName} does not have access type ${accessType}`,
          ),
        );
      }

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

      const originalDescriptor = descriptor;
      const spiedProperty = createSpy(propertyName, descriptor[accessType]);
      let restoreStrategy;

      if (Object.prototype.hasOwnProperty.call(obj, propertyName)) {
        restoreStrategy = function () {
          Object.defineProperty(obj, propertyName, originalDescriptor);
        };
      } else {
        restoreStrategy = function () {
          delete obj[propertyName];
        };
      }

View on GitHub (pinned to 8e6d128e4a)