Jasmine でモックオブジェクトのプロパティをスパイする
モックオブジェクトのプロパティをスパイする際に発生する Jasmine の "already been spied upon" エラーを、Object.getOwnPropertyDescriptor で回避する方法。
Jasmine でテストをしていると、すでにモックされているオブジェクトのプロパティをスパイしようとしたときに、以下のようなエラーに遭遇することがあります。
Error: <spyOnProperty> : currentUser#get has already been spied uponUsage: 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.
ゲッターやセッターをスパイする作業を簡略化するために、以下のような再利用可能なヘルパー関数を定義できます。
/* 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 は、新しいスパイを作成しようとするのではなく、その既存のスパイに直接アクセスします。これを小さな spyGetter/spySetter ヘルパーにまとめておけば、このパターンに遭遇するすべてのテストで、プロパティディスクリプタの取得処理を毎回書き直すことなく、同じ一行の修正を再利用できます。
Related posts
モジュール性と再利用性を高める Angular のプロジェクト構成
core、features、shared の各モジュールに依存の方向を定め、機能を独立させつつ再利用可能なコードを一箇所に集約する Angular のフォルダ構成。

AWS Amplify と Eclipse Mosquitto を連携させた MQTT メッセージング
AWS Amplify の PubSub モジュールを AWS IoT Core に接続する前に、ローカルの Eclipse Mosquitto ブローカーでテストする方法。
Cognito User PoolsとOIDCでSlackサインインを実装する
Cognito user poolをOIDC経由でSlackと連携させ、"Sign in with Slack"をAmplifyでNext.jsアプリに組み込みます。
Lambda Web AdapterでFastAPIをAWS Lambdaにデプロイする
Lambda Web Adapterを使うと、FastAPIで書いたAPIバックエンドをコンテナのまま単一のLambda関数にデプロイできます。
JestでECMAScript Modulesをテストする
package.json、TypeScript、Jestの設定を連携させ、Jestの"Cannot use import statement outside a module"エラーを解消する方法。
