Jasmine で Mock オブジェクトのプロパティを Spy する方法

Jasmine で Mock オブジェクトのプロパティを Spy する方法

岩佐 孝浩
岩佐 孝浩
4 min read
Angular JavaScript

はじめに

Jasmine でテストを実行する際、すでに Mock されたオブジェクトのプロパティをスパイしようとすると、次のエラーが発生することがあります。

Error: <spyOnProperty> : currentUser#get has already been spied upon
Usage: spyOnProperty(<object>, <propName>, [accessType])

このエラーは、Object.getOwnPropertyDescriptor メソッドを使用することで解決可能です。このブログ記事では、このエラーを回避し、ヘルパー関数を活用してテストコードを改善する方法を解説します。

よくあるシナリオ

以下のテストコードで考えてみましょう。このコードは currentUser にアクセスした際にエラーを引き起こします。

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' });
  });

});

解決方法

公式の Jasmine チュートリアル によると、この問題を解決するには Object.getOwnPropertyDescriptor を使用することが推奨されています。この方法により、Mock オブジェクトに作成されたスパイにアクセスできます。

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.

ヘルパー関数

Getter や Setter をスパイする処理を簡略化するため、以下のような再利用可能なヘルパー関数を定義します。

/* 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;
};

これらの関数を src/app/tests/helper.ts のようなユーティリティファイルに配置します。

修正後のテストコード

spyGetter ヘルパー関数を使用すると、テストコードはより簡潔で読みやすくなります。

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'});
   });

修正後のコードでは、currentUser プロパティに対するスパイに spyGetter を使用することで、エラーが発生しなくなっています。

まとめ

Object.getOwnPropertyDescriptor を活用し、スパイ用のユーティリティ関数を作成することで、Jasmine のテストを効率化し、よくあるエラーを効果的に解決できます。このアプローチを採用することで、テストコードのメンテナンス性と可読性を向上させることができます。

Happy Coding! 🚀

岩佐 孝浩

岩佐 孝浩

Software Developer at KAKEHASHI Inc.
AWS を活用したクラウドネイティブ・アプリケーションの要件定義・設計・開発に従事。 株式会社カケハシで、処方箋データ収集の新たな基盤の構築に携わっています。 Japan AWS Top Engineers 2020-2023