Jasmine Spy on Properties on Mock Objects
When attempting to spy on a property of an already mocked object, developers should see the following error.
Error: <spyOnProperty> : currentUser#get has already been spied upon
Usage: spyOnProperty(<object>, <propName>, [accessType])
This error is avoidable with Object.getOwnPropertyDescriptor
.
Situation
When implementing the testing code like the following, the error was raised.
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 tutorial says that we can use Object.getOwnPropertyDescriptor
.
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.
This was also referred to in a Stack Overflow question.
Helper Functions
I wrote helper functions and placed it in 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
After updating my testing code, it ran successfully.
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'});
});
});
The difference is as follows:
--- Sun Sep 26 11:05:21 2021 UTC
+++ Sun Sep 26 11:05:21 2021 UTC
@@ -1,6 +1,7 @@
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', () => {
@@ -18,7 +19,7 @@
});
it('should get the currentUser', () => {
- spyOnProperty(service, 'currentUser').and.returnValue({id: 1, name: 'Hello World'});
+ spyGetter(service, 'currentUser').and.returnValue({id: 1, name: 'Hello World'});
// Testing code here...
expect(service.currentUser).toEqual({id: 1, name: 'Hello World'});
});
Conclusion
When you want to spy on a property of an already mocked object, you can use Object.getOwnPropertyDescriptor
.
I hope you will find this post useful.