Spying on Mock Object Properties in Jasmine
Working around Jasmine's "already been spied upon" error when spying on a mocked object's property with Object.getOwnPropertyDescriptor.
When testing with Jasmine, you might encounter the following error while attempting to spy on a property of an already mocked object:
Error: <spyOnProperty> : currentUser#get has already been spied uponUsage: spyOnProperty(<object>, <propName>, [accessType])This error can be resolved with the Object.getOwnPropertyDescriptor method.
Common Scenario
Consider the following test. Calling spyOnProperty for currentUser raises the error because the property is already a spy.
import { HttpClientTestingModule } from '@angular/common/http/testing';import { TestBed } from '@angular/core/testing';import { AuthService } from '@core/services/auth.service';
describe('AuthService', () => {
let service: jasmine.SpyObj<AuthService>;
beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ { provide: AuthService, useValue: jasmine.createSpyObj('AuthService', [], ['currentUser']) }, ], });
service = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>; });
it('should get the currentUser', () => { spyOnProperty(service, 'currentUser').and.returnValue({ id: 1, name: 'Hello World' }); // Testing code here... expect(service.currentUser).toEqual({ id: 1, name: 'Hello World' }); });
});Solution
The official Jasmine tutorial suggests using Object.getOwnPropertyDescriptor to overcome this issue.
You can create a spy object with several properties on it quickly by passing an array or hash of properties as a third argument to createSpyObj. In this case you won’t have a reference to the created spies, so if you need to change their spy strategies later, you will have to use the Object.getOwnPropertyDescriptor approach.
To simplify the process of spying on getters and setters, you can define reusable helper functions like this:
/* eslint-disable-next-line arrow-body-style */export const spyGetter = <T, K extends keyof T>(target: jasmine.SpyObj<T>, key: K): jasmine.Spy => { return Object.getOwnPropertyDescriptor(target, key)?.get as jasmine.Spy;};
/* eslint-disable-next-line arrow-body-style */export const spySetter = <T, K extends keyof T>(target: jasmine.SpyObj<T>, key: K): jasmine.Spy => { return Object.getOwnPropertyDescriptor(target, key)?.set as jasmine.Spy;};Updated Testing Code
With the helper function spyGetter, the testing code becomes more concise and readable:
import { HttpClientTestingModule } from '@angular/common/http/testing';import { TestBed } from '@angular/core/testing';import { AuthService } from '@core/services/auth.service';import { spyGetter } from '@tests/helper';
describe('AuthService', () => {
let service: jasmine.SpyObj<AuthService>;
beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ { provide: AuthService, useValue: jasmine.createSpyObj('AuthService', [], ['currentUser']) }, ], });
service = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>; });
it('should get the currentUser', () => { spyGetter(service, 'currentUser').and.returnValue({ id: 1, name: 'Hello World' }); // Testing code here... expect(service.currentUser).toEqual({ id: 1, name: 'Hello World' }); });
});Conclusion
Use Object.getOwnPropertyDescriptor instead of calling spyOnProperty again when a property spy was created by jasmine.createSpyObj.
Passing ['currentUser'] as the third argument to jasmine.createSpyObj creates the property spy before the test runs. A later spyOnProperty call therefore attempts to create a duplicate spy and fails.
Object.getOwnPropertyDescriptor(target, key)?.get returns the existing getter spy. Wrapping this lookup in spyGetter and spySetter helpers keeps the test code concise and reusable.
Related posts
Angular Project Structure for Modularity and Reusability
Organize an Angular application into core, features, and shared directories with clear dependency boundaries.

Integrating AWS Amplify with Eclipse Mosquitto for MQTT Messaging
Testing AWS Amplify's PubSub module against a local Eclipse Mosquitto broker before pointing it at AWS IoT Core.
Sign in with Slack Using Cognito User Pools and OIDC
Federating Cognito user pools with Slack over OIDC and wiring "Sign in with Slack" into a Next.js app with Amplify.
Deploying FastAPI on AWS Lambda with Lambda Web Adapter
Containerizing a FastAPI backend and deploying it to a single Lambda function with Lambda Web Adapter and AWS CDK.
Testing ECMAScript Modules with Jest
Fixing Jest's "Cannot use import statement outside a module" error by wiring up ESM support across package.json, TypeScript, and Jest config.
