How to Effectively Spy on Mock Object Properties in Jasmine

How to Effectively Spy on Mock Object Properties in Jasmine

Takahiro Iwasa
Takahiro Iwasa
3 min read
Angular JavaScript

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! 🚀

Takahiro Iwasa

Takahiro Iwasa

Software Developer at KAKEHASHI Inc.
Involved in the requirements definition, design, and development of cloud-native applications using AWS. Now, building a new prescription data collection platform at KAKEHASHI Inc. Japan AWS Top Engineers 2020-2023.