プログラミング

テストダブル (Test Double) とは

Jestを勉強する前にテストダブルという用語が出てきたので、それについて調べたことなどをまとめてみました。

テストダブル(Test Double)とは

テストダブル (Test Double) とは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと。ダブルは代役、影武者を意味する。

Wikipedia

テストダブルには下記5つが含まれます。

  • Dummy
  • Spy
  • Stub
  • Mock
  • Fake

※Fakeは主にバックエンドで使われるものみたいです。

それでは、テストダブルの5つ(Dummy、Spy、Stub、Mock、Fake)について詳しく見ていきます。

Dummy

テスト対象とは関係ないが、ある関数やクラスを使うために引数が足りない場合などに使用されるものです。

具体的な例

下記は、クラスをインスタンス化する際に必要なパラメータをDummyで用意している例です。
テスト対象はクラスのインスタンスの動作ですが、インスタンス化するにはパラメータが必要なのでDummyで引数を渡しています。

Class User {
	/**/
}

// jest
test('some test', () => {
	// テスト対象ではないが、パラメータが無いとテストできない場合、パラメータにDummyを使う
	new User({ name: 'Bob', age: 23 })

  // テストコード
})

下記はcreateUser()の引数であるagreementがfalseの場合、検証したいコードが実行できないため、Dummyでagreementにtrueを入れています。

function createUser({ data, agreement }) {
  // agreement(規約に同意したか)をチェック
	if (!agreement) return;

	// 検証したいコード
}

// jest
test('some test', () => {
	const agreement = true; // Dummy(これがないとテストしたい関数が動かないため)
	createUser({ name: 'Bob', age: 23 }, agreement);
})

Spy

テスト対象からの「間接的な出力」を検証するために使います。
出力を記録しておくことで、テストコードの実行後に、値を取り出して検証できます。

例えば下記のようなことができます

  • メソッドの引数のチェック
  • メソッドが呼ばれた回数のチェック

具体的な例

たとえばvideoオブジェクトにplay()があったとします。

const video = {
  play() {
    return true;
  },
};

module.exports = video;

テストコードでは、Jest.spyOn()を使用してvideoオブジェクトのplay()の実行状況を監視させます。

そして実際にvideo.play()を実行したとき、テストコードでspyしたplay()が実行されたのか、何回実行されたのかなど、詳細なテストをすることができます。

const video = require('./video');

// Jestを使用
test('plays video', () => {
  const spy = jest.spyOn(video, 'play');
  const isPlaying = video.play();

  expect(spy).toHaveBeenCalled(); // videoのplay()が実行されたか
  expect(spy).toHaveBeenCalledTimes(1); // videoのplay()1回呼ばれたか
  expect(isPlaying).toBe(true);
});

Stub

テスト対象に必要な情報だけを返すダミーオブジェクトです。
現在のテストに必要なデータを返すだけで、実際のデータを返す必要はありません。

つまり、テスト対象と関係ない関数などの返却値を上書きすることができます。
たとえ関数などが未実装であっても大丈夫です。
そのため、テスト対象を確実にテストできるようになります。

※テスト対象と関係ないところがエラーになって、テスト対象のテストが失敗するのを避けるのが目的です。

具体的な例

下記は、テストコードにてJestのmockImplementation()を使用して、someApi.create()が返却する値を常にtrueに上書きしている例です。

function someApi() {
	return {
		create() { /* 依存先のメソッドが空でも問題なし */ },
		search() { /*  */ }
	}
}

// テストコード
test('test', async (() => {
	const name = 'Bob';
  const spy = jest.spyOn(someApi, 'create').mockImplementation(() => {
		return true; // someApi.create()がtrueを返却するように上書き
	});

	const res = await someApi.create({ name });

  expect(spy).toHaveBeenCalled();
  expect(spy).toHaveBeenCalledTimes(1);
}));

まだ未実装の関数や、テスト対象のスコープ外の関数などの挙動を上書きできるので便利ですね。

Mock

テスト対象が依存するコンポーネントの振る舞いを制御するために使用されます。
テスト対象がコンポーネントを呼び出したとき、Mockは代わりに設定された動作を返します。

テストダブルのMockとJestの考え方の違いは下記のとおりです。

テストダブルのMockの考え方

  1. 事前に期待される動作を定義
  2. テスト対象を実行
  3. モック内で自動検証

※JestにはMockは無い

Jestの考え方

  1. テスト対象を実行
  2. 期待値と結果をテストコード内で実行
    expect(期待値).toBe(結果)など

具体的な例

"test should call all subscribers when exceptions": function () {
    var myAPI = { method: function () {} };

    var spy = sinon.spy();
    var mock = sinon.mock(myAPI); // mockを作成
    mock.expects("method").once().throws(); // mockのmethodが1回呼ばれるかなどを定義

    PubSub.subscribe("message", myAPI.method);
    PubSub.subscribe("message", spy);
    PubSub.publishSync("message", undefined);

    mock.verify(); // mockを検証
    assert(spy.calledOnce);
}

Fake

実際のテスト対象と同様の動作をするが、テストのために簡易化・軽量化されたもの。
※フロントエンドではあまり使われない

具体的な例

  • RDBの代わりにインメモリデータベース
  • AWSの代わりにLocalStack

まとめ

テストダブルについて、おおよそ全体像は理解できました。
実際にはJest等を用いてテストコードを書いていくうちに理解が深まると思います!