{"record":{"id":"2d98723865e0dcc4","repo":"jestjs/jest","slug":"propertyname-has-already-been-spied-upon","errorCode":null,"errorMessage":"${propertyName} has already been spied upon","messagePattern":"(.+?) has already been spied upon","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/jest-jasmine2/src/jasmine/spyRegistry.ts","lineNumber":195,"sourceCode":"      if (!descriptor.configurable) {\n        throw new Error(\n          getErrorMsg(`${propertyName} is not declared configurable`),\n        );\n      }\n\n      if (!descriptor[accessType]) {\n        throw new Error(\n          getErrorMsg(\n            `Property ${propertyName} does not have access type ${accessType}`,\n          ),\n        );\n      }\n\n      if (obj[propertyName] && isSpy(obj[propertyName])) {\n        if (this.respy) {\n          return obj[propertyName];\n        } else {\n          throw new Error(\n            getErrorMsg(`${propertyName} has already been spied upon`),\n          );\n        }\n      }\n\n      const originalDescriptor = descriptor;\n      const spiedProperty = createSpy(propertyName, descriptor[accessType]);\n      let restoreStrategy;\n\n      if (Object.prototype.hasOwnProperty.call(obj, propertyName)) {\n        restoreStrategy = function () {\n          Object.defineProperty(obj, propertyName, originalDescriptor);\n        };\n      } else {\n        restoreStrategy = function () {\n          delete obj[propertyName];\n        };\n      }","sourceCodeStart":177,"sourceCodeEnd":213,"githubUrl":"https://github.com/jestjs/jest/blob/8e6d128e4a278059ecddecaa97400b04c8ae5fd9/packages/jest-jasmine2/src/jasmine/spyRegistry.ts#L177-L213","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Restore between tests: afterEach(() => jest.restoreAllMocks()).","Consolidate the property spy into a single beforeEach and remove duplicates.","Opt into respy only if intentional: jasmine.getEnv().allowRespy(true).","Set restoreMocks:true in the Jest config to reset spies automatically."],"exampleFix":"// before\nbeforeEach(() => spyOn(config, 'url', 'get'));\nit('spies again', () => spyOn(config, 'url', 'get')); // throws\n// after\nbeforeEach(() => jest.spyOn(config, 'url', 'get'));\nafterEach(() => jest.restoreAllMocks());","handlingStrategy":"validation","validationCode":"afterEach(() => jest.restoreAllMocks());\n// before re-spying, restore\nif (obj[prop] && (obj[prop] as any).and instanceof Object) {\n  (obj[prop] as any).and.restore?.();\n}\nspyOn(obj, prop, 'get');","typeGuard":"const isPropertySpy = (v: unknown): boolean =>\n  v != null && typeof (v as any).and === 'object';","tryCatchPattern":null,"preventionTips":["Set restoreMocks:true in Jest config.","Consolidate property spies into a single beforeEach.","Use jest.restoreAllMocks in afterEach."],"tags":["jest","spyon","mocking","property-accessor","duplicate-spy","test-isolation"],"backgroundTag":null,"analyzedSha":"8e6d128e4a278059ecddecaa97400b04c8ae5fd9","analyzedAt":"2026-08-10T18:11:27.960Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}