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 にアクセスした際にエラーを発生させます。

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

まとめ

spyOnProperty を直接呼び出す代わりに Object.getOwnPropertyDescriptor を使うことで、Jasmine でモックされたプロパティをテストする際の “already been spied upon” エラーを解消できました。jasmine.createSpyObj の第 3 引数として ['currentUser'] を渡すこと自体が、テストが明示的に spyOnProperty を呼び出すより前に、暗黙のうちにプロパティスパイを作成してしまっている原因です。だからこそ、2 回目の呼び出しがすでに存在するスパイと衝突してしまうのです。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.