Jasmine でモックオブジェクトのプロパティをスパイ

Jasmine でモックオブジェクトのプロパティをスパイ

Takahiro Iwasa
(岩佐 孝浩)
Takahiro Iwasa (岩佐 孝浩)
3 min read
Angular JavaScript

既にモックされたオブジェクトのプロパティをスパイしようとすると、以下のエラーが表示されるはずです。

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

Object.getOwnPropertyDescriptor を使用して、このエラーを回避できます。

状況

以下のようなテストコードを実装すると、エラーが発生しました。

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

});

解決方法

公式のチュートリアルによれば、 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.

これは Stack Overflow の質問でも言及されていました。

ヘルパー関数

ヘルパー関数を 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;
};

テストコード更新

テストコードを更新した後、正常に実行されました。

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'});
   });
Takahiro Iwasa
(岩佐 孝浩)

Takahiro Iwasa (岩佐 孝浩)

Software Developer at iret, Inc.
主に AWS を利用したクラウドネイティブアプリケーションの設計および開発をしています。 Japan AWS Top Engineers 2020-2023