Spying on Mock Object Properties in Jasmine

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.

Takahiro Iwasa
3 min read

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 upon
Usage: 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:

src/app/tests/helper.ts
/* 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.

About the author

Takahiro Iwasa

Takahiro Iwasa

Software Developer

This blog shares technical notes from hands-on projects—architecture, implementation, and AWS service integrations.