Jasmine で Mock オブジェクトのプロパティを Spy する方法
はじめに
Jasmine でテストを実行する際、すでに Mock されたオブジェクトのプロパティをスパイしようとすると、次のエラーが発生することがあります。
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
を使用することが推奨されています。この方法により、Mock オブジェクトに作成されたスパイにアクセスできます。
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.
ヘルパー関数
Getter や Setter をスパイする処理を簡略化するため、以下のような再利用可能なヘルパー関数を定義します。
/* 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;
};
これらの関数を src/app/tests/helper.ts
のようなユーティリティファイルに配置します。
修正後のテストコード
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' });
});
});
--- 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'});
});
修正後のコードでは、currentUser
プロパティに対するスパイに spyGetter
を使用することで、エラーが発生しなくなっています。
まとめ
Object.getOwnPropertyDescriptor
を活用し、スパイ用のユーティリティ関数を作成することで、Jasmine のテストを効率化し、よくあるエラーを効果的に解決できます。このアプローチを採用することで、テストコードのメンテナンス性と可読性を向上させることができます。
Happy Coding! 🚀