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 がすでにスパイとして作成されているため、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.
ゲッターやセッターをスパイする作業を簡略化するために、以下のような再利用可能なヘルパー関数を定義できます。
/* 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 は、既存のゲッタースパイを返します。この処理を spyGetter と spySetter にまとめると、テストコードを簡潔に保ちながら再利用できます。
Related posts
モジュール性と再利用性を高める Angular のプロジェクト構成
Angular アプリケーションを core、features、shared に分け、依存関係の境界を明確にするフォルダ構成を紹介します。

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 にデプロイする
FastAPI で書いた API バックエンドをコンテナ化し、Lambda Web Adapter と AWS CDK を使って単一の Lambda 関数へデプロイします。
Jest で ECMAScript Modules をテストする
package.json、TypeScript、Jest の設定を連携させ、Jest の "Cannot use import statement outside a module" エラーを解消する方法を紹介します。
