TestDouble ≠ mock
これまでTestDoubleとかmockとかspyとかstubとかよくわからず使ってました。
なんなら全部mockを言い換えた言葉なんでしょ! って認識でした
どうやら違うらしいです。l
改めてTestDoubleについて学んだのでまとめます。
そもそもTestDoubleって?
Test Double は、ソフトウェアテストにおいて使用される
stub,dummy,spy,mock,fakeなどを指します。
これらは、実際のオブジェクトの代わりに使用される、テスト用の代替オブジェクトです。
これらのオブジェクトを使用することで、テスト対象となるオブジェクトと直接的な依存関係を避けることができ、
実際のオブジェクトを使用することによって発生する問題(例えば、データベースへの書き込みなど)を回避することができます。
また、テスト用のオブジェクトを使用することで、実際のオブジェクトの返す値を制御し、期待通りの処理を行っているかを検証することもできます。
ってChatGPTが言ってました。頭いいですね。
つまり
stub,dummy,spy,mock,fakeを総称したものをTestDoubleっていうんですね
その中でも特に使用頻度の高いであろう
stubとspyとmockについてコードを使って説明してみます。
Stub
Stub は、Test Double の一種です。Stub は、テスト対象のオブジェクトとの関連付けを持つ依存関係を避けるために使用されます。
Stub は、テスト対象のオブジェクトから呼び出されるメソッドなどを代替するオブジェクトであり、予め定義された返り値を返すようになっています。
この仕組みによって、テスト対象のオブジェクトが呼び出す依存関係を持つオブジェクトの返り値を制御することができます。
この説明ではよくわからないのでコードを使って説明します。
jestを使っています。
// sample.ts
import { DependentObject } from './dependentObject'
export class SampleService {
constructor(public dependentObject: DependentObject) {
}
getData() {
return this.dependentObject.getData();
}
}
↑SampleServiceのgetDataメソッドはdependentObjectに依存しています。
// dependentObject.ts
export class DependentObject {
getData() {
return 'real data';
}
}
↑dependentObjectは実際だと外部APIなどの時間がかかる処理や頻繁に試せない処理
または未実装の処理になると思います。
なので今回はこのdependentObjectを使わずにSampleServiceのテストを書いていくこととします。
// sample.test.ts
import { SampleService } from './sample';
describe('SampleService', () => {
it('getDataメソッド実行時stubで指定したデータが返ること', () => {
// Stub の生成
const dependentObject = {
getData: jest.fn().mockReturnValue('stub data')
};
//SampleServiceのインスタンス化
const sampleService = new SampleService(dependentObject);
const result = sampleService.getData();
//sampleService.getData() で使用されるdependentObject はstubになるので ’stub Data’ を返すはず
expect(result).toBe('stub data');
});
});
jestを使うことでこのように簡単にstubを記述することができます。
Spy
Spyは、Test Double の一種です。Spyは特定の関数が正常に呼び出されているかどうかを確認するために使用されます。
コードを使って説明していきます。
※テスト対象のクラスと依存クラスは先ほどのstubと全く同じになるので省略します。
// sample.test.ts
import { SampleService } from './sample';
describe('SampleService', () => {
it('getDataメソッド実行時dependentObject.getDataメソッドが実行されていること', () => {
// Stub の生成
const dependentObject = {
getData: jest.fn().mockReturnValue('stub data')
};
//SampleServiceのインスタンス化
const sampleService = new SampleService(dependentObject);
const result = sampleService.getData();
//jestで生成された関数内に関数の呼び出し確認用のプロパティが格納されている
expect(dependentObject.getData.mock.calls.length).toBe(1)
});
});
spyもjestを使えば簡単に使えますね!
Mock
Mockとは、自己検証Spyのことです。
Mock自身にアサーションが含まれています。
1つのテストケースに対して、確認する項目が複数あるときなど、一連の確認項目を1つのメソッドで一括して確認します。
ただ、最初からMockを作成するのではなく
spyのリファクタリング結果がmockになるイメージです。
分かりやすいようにこれまでとコードを少し変えてます。
// sample.ts
import { DependentObject } from './dependentObject'
export class SampleService {
constructor(public dependentObject: DependentObject,public password: string) {
}
getData() {
if(this.password === 'a0000'){
return this.dependentObject.getData();
}
return this.dependentObject.errorData();
}
}
↑パスワードを引数にとるようにしてパスワードが正しい場合はdependentObject.getData()を実行。
パスワードが間違っている場合はdependentObject.errorData()を実行するように実装します。
// sample.ts
export class DependentObject {
getData() {
return 'real data';
}
errorData() {
return 'passwordが違います';
}
}
↑dependentObjectにerrorDataメソッドを追加実装しました。
これまでと同じく実際だと外部APIなどの時間がかかる処理や頻繁に試せない処理または未実装の処理になると思います。
今回もこのdependentObject使わずにSampleServiceのテストを実装します
テストの内容は
1.正しいパスワードが入力された時はdependentObjectのgetDataメソッドが実行され
かつ、errorDataメソッドは実行されたいないことを確認。
2.間違ったパスワードが入力された時はdependentObjectのerrorDataメソッドが実行され
かつ、getDataメソッドは実行されたいないことを確認。
以上二つのテストになります。
まずはこれまで通りStubとSpyを使用して記述してみます。
describe('SampleService', () => {
it('正しいpasswordの時はdependentObjectのgetDataメソッドが実行されerrorDataメソッドは実行されていないこと', () => {
// Stub の生成
const dependentObject = {
getData: jest.fn().mockReturnValue('stub data'),
errorData: jest.fn().mockReturnValue('stub error data')
};
//SampleServiceのインスタンス化
const sampleService = new SampleService(dependentObject,"a0000");
const result = sampleService.getData();
//sampleService.getData() で使用されるdependentObject はstubになるので ’stub Data’ を返すはず
expect(dependentObject.getData.mock.calls.length).toBe(1)
expect(dependentObject.errorData.mock.calls.length).toBe(0)
});
it('間違ったpasswordの時はdependentObjectのerrorDataメソッドが実行さgetDataメソッドは実行されていないこと', () => {
// Stub の生成
const dependentObject = {
getData: jest.fn().mockReturnValue('stub data'),
errorData: jest.fn().mockReturnValue('stub error data')
};
//SampleServiceのインスタンス化
const sampleService = new SampleService(dependentObject,"b0000");
const result = sampleService.getData();
//sampleService.getData() で使用されるdependentObject はstubになるので ’stub Data’ を返すはず
expect(dependentObject.getData.mock.calls.length).toBe(0)
expect(dependentObject.errorData.mock.calls.length).toBe(1)
});
});
まぁこれでもいいんですが同じようなことがそれぞれのテストで書かれていて少し冗長ですね
リファクタリングしてみましょう!
// sample.test.ts
import { SampleService } from './stub';
import { DependentObject } from './dependentObject'
class MockDependentObject implements DependentObject{
private getDataWasCalled = false
private errorDataWasCalled = false
getData() {
this.getDataWasCalled = true
return 'real data';
}
errorData() {
this.errorDataWasCalled = true
return 'passwordが違います';
}
verifyGetData():void{
expect(this.getDataWasCalled).toBeTruthy()
expect(this.errorDataWasCalled).toBeFalsy()
}
verifyErrorData():void{
expect(this.getDataWasCalled).toBeFalsy()
expect(this.errorDataWasCalled).toBeTruthy()
}
}
describe('SampleService', () => {
it('正しいpasswordの時はdependentObjectのgetDataメソッドが実行されerrorDataメソッドは実行されていないこと', () => {
const mockDependentObject = new MockDependentObject()
const sampleService = new SampleService(mockDependentObject,"a0000");
const result = sampleService.getData();
mockDependentObject.verifyGetData()
});
it('間違ったpasswordの時はdependentObjectのerrorDataメソッドが実行さgetDataメソッドは実行されていないこと', () => {
const mockDependentObject = new MockDependentObject()
const sampleService = new SampleService(mockDependentObject,"a0000");
const result = sampleService.getData();
mockDependentObject.verifyErrorData()
});
});
classを同じファイル内に書いちゃってるのでリファクタリング後の方が長くなってしまいました 笑
でもテストの方はだいぶスッキリしたと思います。
MockDependentObjectをclassにしてインスタンス化したものをそれぞれのテストで使用しています。
MockDependentObjectのverifyGetDataとverifyErrorData内でexpectしているので
テスト内ではそれぞれのメソッドを呼ぶだけでテストが完了するという仕様になってます。
まとめ
TestDoubleのそれぞれの役割を改めて認識できました。
まだまだ浅い知識ですので間違いありましたらご指摘お願いします。
参考
以下ページを参考にさせていただきました。