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 testing code. This code raises the error when currentUser is accessed.

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

Replacing a direct spyOnProperty call with Object.getOwnPropertyDescriptor resolved the “already been spied upon” error when testing a mocked property in Jasmine. Passing ['currentUser'] as the third argument to jasmine.createSpyObj is what silently creates a property spy before a test ever calls spyOnProperty explicitly, which is why the second call collides with one that already exists. Object.getOwnPropertyDescriptor(target, key)?.get reaches that existing spy directly instead of trying to create a new one, and wrapping it in a small spyGetter/spySetter helper means every test that hits this pattern can reuse the same one-line fix rather than re-deriving the property descriptor lookup each time.

About the author

Takahiro Iwasa

Takahiro Iwasa

Software Developer

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