How to Effectively Spy on Mock Object Properties in Jasmine
Introduction
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. This blog post will guide you through avoiding this error and improving your test code using helper functions.
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. This approach ensures you have access to the spies created on the mock object.
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.
Helper Functions
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;
};
Place these functions in a utility file, such as src/app/tests/helper.ts
.
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' });
});
});
--- 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'});
});
This updated code uses spyGetter
for spying on the currentUser
property, eliminating the error and maintaining a clean structure.
Conclusion
By leveraging Object.getOwnPropertyDescriptor
and creating utility functions for property spying, you can streamline your Jasmine tests and resolve common errors effectively. This approach improves the maintainability and readability of your test code.
Happy Coding! 🚀