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

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

モックオブジェクトのプロパティをスパイする際に発生する Jasmine の "already been spied upon" エラーを、Object.getOwnPropertyDescriptor で回避する方法を解説します。

Takahiro Iwasa
4 min read

Jasmine でテストをしていると、すでにモックされているオブジェクトのプロパティをスパイしようとしたときに、以下のようなエラーに遭遇することがあります。

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

このエラーは Object.getOwnPropertyDescriptor メソッドを使うことで解決できます。

よくあるシナリオ

以下のテストでは、currentUser がすでにスパイとして作成されているため、spyOnProperty を呼び出すとエラーが発生します。

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 を使うことが提案されています。

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.

ゲッターやセッターをスパイする作業を簡略化するために、以下のような再利用可能なヘルパー関数を定義できます。

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

更新後のテストコード

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

まとめ

jasmine.createSpyObj ですでにプロパティスパイを作成している場合は、spyOnProperty を再度呼び出さず、Object.getOwnPropertyDescriptor を使用します。

jasmine.createSpyObj の第 3 引数に ['currentUser'] を渡すと、テストの実行前にプロパティスパイが作成されます。その後に spyOnProperty を呼び出すと、同じプロパティにスパイを重複して作成しようとして失敗します。

Object.getOwnPropertyDescriptor(target, key)?.get は、既存のゲッタースパイを返します。この処理を spyGetterspySetter にまとめると、テストコードを簡潔に保ちながら再利用できます。

About the author

Takahiro Iwasa

Takahiro Iwasa

Software Developer

This blog shares technical notes from hands-on projects—architecture, implementation, and AWS service integrations.