【テストダブル】スタブ・スパイ・モック・フェイク・ダミーの特徴と違い

この記事では『テストダブル』について、

  • テストダブルとは
  • テストダブルの種類・分類
    • スタブの特徴とプログラム例
    • スパイの特徴とプログラム例
    • モックの特徴とプログラム例
    • フェイクの特徴とプログラム例
    • ダミーの特徴とプログラム例

などを図を用いて分かりやすく説明するように心掛けています。ご参考になれば幸いです。

テストダブルとは

テストダブルとは

テストダブルは「テスト対象が依存しているコンポーネント(依存コンポーネント)に置き換えたもの」や「依存コンポーネントの振る舞いを監視したり制御したりするもの」です。

例えば、次のようなプログラムがあるとします。

// テスト対象の関数
// テスト対象は外部メソッド()に依存している
function テスト対象() {
    ...
    外部メソッド();
    ...
    return hoge;
}

// テストコード
it('テスト対象()の戻り値が規定値であることを確認する', function () {
    assert.strictEqual(テスト対象(), 期待値);
});

このプログラムでは、テスト対象()の戻り値が期待値と等しいかをテストしています。また、テスト対象()外部メソッド()を内部で使用しているため、テスト対象()外部メソッド()に依存しています。

しかし、依存コンポーネント(上記のプログラムでいう外部メソッド())が「テスト環境で使用できない場合」、「実際に動作させると問題を引き起こす場合」、または「開発途中でまだ完成していない場合」など、テストで依存コンポーネントをそのまま使うことができない状況があります。

このような場合にテストダブルを使用します。テストダブルを使用して、依存コンポーネントを置き換えたり、依存コンポーネントの振る舞いを監視または制御したりすることで、テスト対象()のテストを容易にし、テスト結果の信頼性を向上させます。

テストダブルの由来

例えば、映画製作などでは、俳優が怪我をしないように危険なシーンの代わりをする方を「スタンドダブル」、難しいダンスの披露を代わりにする方を「ダンスダブル」と呼びます。これらの「ダブル」はオリジナルの俳優の特定の役割を果たすことで、安全性を確保したり、品質を高めたりします。

同じ考え方をソフトウェアテストに適用したものが「テストダブル」です。ソフトウェアテストにおいては、特定のオブジェクトや関数の代わりを務めるものを「テストダブル」と呼びます。テストダブルを用いることで、特定のシナリオを再現したり、テストが難しい部分を簡単したりすることができます。

テストダブルの種類・分類

間接入力・間接出力とは

テストダブルには様々な種類があり、xUnit Test Patternsによる分類が有名です。

xUnit Test Patternsではテストダブルを下記の5つに分類しています。

  • スタブ(Test Stub)
  • スパイ(Test Spy)
  • モック(Mock Object)
  • フェイク(Fake Object)
  • ダミー(Dummy Object)

各テストダブルの特徴を上図に示します。各テストダブルの特徴を理解しておくと、テストコードを記述する時に、どの種類のテストダブルを選択するべきなのかが明確になります。

間接入力・間接出力とは

間接入力・間接出力とは

テストダブルは主に「間接入力(テスト対象への入力)」と「間接出力(テスト対象からの出力)」の扱い方で分類されています。各テストダブルの特徴を説明する前にまず、「間接入力」と「間接出力」について説明します。

間接入力

間接入力は「依存コンポーネント」から「テスト対象」への入力であり、テストコードから間接入力を直接見ることはできません。

以下に間接入力に関するプログラム例を示しています。テスト対象()は、依存コンポーネント(ここでは、外部メソッド())を呼び、その戻り値を変数indirectInputに代入しています。

// テスト対象の関数
// テスト対象は外部メソッド()に依存している
function テスト対象() {
    ...
    indirectInput = 外部メソッド();
    ...
    return hoge;
}

// テストコード
it('テスト対象()の戻り値が規定値であることを確認する', function () {
    assert.strictEqual(テスト対象(), 期待値);
});

上記のプログラムで分かるように、外部メソッド()の戻り値は、テストコードからは直接見ることができません。また、外部メソッド()の戻り値はテスト対象()に影響を与えています。

このようにテストコードからは直接見えないが、「テスト対象」に影響を与える入力を間接入力といいます。

間接出力

間接出力は、「テスト対象」から「依存コンポーネント」への出力であり、テストコードから間接出力を直接見ることはできません。

以下に間接が分かるプログラム例を示しています。テスト対象()は、依存コンポーネント(ここでは、外部メソッド())を呼び、その際に引数indirectOutputを外部メソッドに渡しています。

// テスト対象の関数
// テスト対象は外部メソッド()に依存している
function テスト対象() {
    ...
    外部メソッド(indirectOutput);
    ...
    return hoge;
}

// テストコード
it('テスト対象()の戻り値が規定値であることを確認する', function () {
    assert.strictEqual(テスト対象(), 期待値);
});

上記のプログラムで分かるように、外部メソッド()への引数は、テストコードからは直接見ることができません。

このように、テストコードからは直接見えないが、テスト対象()が外部メソッド()に出力しているものを間接出力といいます。

間接出力の例としては以下のようなものがあります。

  • テスト対象が依存コンポーネントへ渡す引数
  • テスト対象が依存コンポーネントを呼び出す回数や順序

では次に、各テストダブル(スタブ、スパイ、モック、フェイク、ダミー)の特徴について順番に説明します。

スタブ

スタブ

スタブは、テスト対象が依存しているコンポーネント(依存コンポーネント)の代替品であり、事前設定した間接入力をテスト対象に提供するテストダブルです。

間接入力(テスト対象への入力)をテストコード上で事前設定して、テスト対象をテストします。スタブがテスト対象に渡すデータは、事前設定した間接入力値になります。

スタブは下記の場合に主に使います。

スタブを用いるケース

  • 依存コンポーネントが未完成の場合
    • 例えば、依存コンポーネントが開発途中で未完成の場合、テスト対象はその依存コンポーネントを呼び出すことができません。この場合、依存コンポーネントの代替品である「スタブ」を作成して、あらかじめ設定した固定データを返すようにします。
  • 依存コンポーネントが「時間のかかる処理」や「実行できない処理」や「実際に動作させると問題を引き起こす処理」の場合
    • 例えば、依存コンポーネントがネットワークを介した通信(例:天気予報のAPIなど)を行っている場合を考えてみましょう。インターネットに接続せずにテストしたい場合、依存コンポーネントは天気予報のAPIを呼び出すことができません。この場合、あらかじめ設定した固定のデータ(例:晴れ)を返す「スタブ」を作成します。これにより、テスト対象は依存コンポーネントの影響を受けずに、テスト対象のコードの動作のみを検証することができます。
  • 依存コンポーネントが「毎回実行結果が変わる処理」の場合
    • 例えば、依存コンポーネントが時刻を返す場合を考えてみましょう。テスト対象が依存コンポーネントの戻り値によって、処理を変えていると、時刻によってテスト結果が異なってしまいます。この場合、「スタブ」を作成して、あらかじめ設定した固定データを返すことで、依存コンポーネントの影響を受けずに、テスト対象が意図通りに動いているかをテストすることができます。

スタブのプログラム例

以下にテストダブルとしてスタブを使用しているプログラム例を示しています。

// ★テスト対象が依存しているgetCurrentHour関数
// getCurrentHour関数では現在の時刻を取得している。
function getCurrentHour() {
    return new Date().getHours();
}

// ★テスト対象のgreet関数
// 現在の時刻に応じて異なる挨拶を返している
function greet() {
    const currentHour = getCurrentHour();
    if (currentHour < 12) {
        return 'おはよう';
    }
    else if (currentHour < 18) {
        return 'こんにちは';
    }
    else {
        return 'こんばんは';
    }
}

// ★テストコード
const sinon = require('sinon');
const assert = require('assert');

describe('Stubのテスト', function () {
    it('時刻によって挨拶が変わることを確認する', function () {
        // greet関数が依存しているgetCurrentHour関数をスタブ化
        // 間接入力(テスト対象への入力)をテストコード上で事前設定(常に10を返す)
        getCurrentHour = sinon.stub().returns(10);
        // greetが「おはよう」を返すことを検証
        assert.strictEqual(greet(), 'おはよう');

        // greet関数が依存しているgetCurrentHour関数をスタブ化
        // 間接入力(テスト対象への入力)をテストコード上で事前設定(常に15を返す)
        getCurrentHour = sinon.stub().returns(15);
        // greetが「こんにちは」を返すことを検証
        assert.strictEqual(greet(), 'こんにちは');

        // greet関数が依存しているgetCurrentHour関数をスタブ化
        // 間接入力(テスト対象への入力)をテストコード上で事前設定(常に20を返す)
        getCurrentHour = sinon.stub().returns(20);
        // greetが「こんばんは」を返すことを検証
        assert.strictEqual(greet(), 'こんばんは');
    });
});

テスト対象のgreet関数は定数currentHourの値によって戻り値が異なります。また、greet関数はgetCurrentHour関数に依存しています。

上記のプログラムの場合、getCurrentHour関数の戻り値が間接入力に相当します。getCurrentHour関数は時刻によって戻り値が異なるので、getCurrentHour関数の戻り値を定数currentHourに代入すると、時刻によってテスト結果が異なってしまいます。

そのため、今回はSinon.JSのstubを用いて、getCurrentHour関数をスタブ化し、常に固定の値を返すようにしています。このようにすることで、依存コンポーネント(getCurrentHour関数)の影響を受けずに、greet関数が意図通りに動いているかをテストすることができます。

スパイ

スパイ

スパイは「テスト対象」から「依存コンポーネント」への出力(間接出力)を監視して記録します。

言い換えると、スパイは依存コンポーネントの呼び出し方(テスト対象が依存コンポーネントを何回呼び出したか、どのような引数で呼び出したか)を監視して記録しています。

依存コンポーネントの呼び出し方(間接出力)の検証はテストコード上で後から行います。

スパイのプログラム例

以下にテストダブルとしてスパイを使用しているプログラム例を示しています。

// ★テスト対象が依存しているdatabaseのgetNameメソッド
const database = {
    getName: function (id) {
        // このメソッドでは、データベースからデータを取得するが、このメソッドが未完成と仮定。
        // そのため、このメソッドでは何もしていない
    }
};

// ★テスト対象のgetUserNameById関数
function getUserNameById(id) {
    return database.getName(id);
}

// ★テストコード
const sinon = require('sinon');
const assert = require('assert');

describe('Spyのテスト', function () {
    it('正しいIDでdatabase.getNameを1度だけ呼び出しているか', function () {

        // Sinon.JSを使ってdatabaseのgetNameメソッドをスパイする
        // 間接出力(databaseのgetNameメソッドが何回呼び出されたか、どのような引数で呼び出されたか)を記録する
        let spy = sinon.spy(database, 'getName');

        // テスト対象の関数を呼び出す
        getUserNameById('123');

        // ★依存コンポーネント(databaseのgetNameメソッド)の呼び出し方(間接出力)の検証はテストコード上で後から行う
        // databaseのgetNameメソッドが一度だけ呼び出されたか
        assert(spy.calledOnce);
        // databaseのgetNameメソッドが引数('123')で呼び出されたか
        assert(spy.calledWith('123'));

        // スパイの復元
        spy.restore();
    });
});

テスト対象のgetUserNameById関数はdatabasegetNameメソッドに依存しています。getNameメソッドは引数idの値を用いて、idに関するデータをデータベースから取得するメソッドですが、getNameメソッドは未完成と仮定します。

Sinon.JSのspyを用いて、databaseのgetNameメソッドをスパイすることで、テスト対象が「databaseのgetNameメソッドを何回呼び出したか、どのような引数で呼び出したか」を監視して記録しています。

また、スパイでは依存コンポーネントの呼び出し方(間接出力)の検証はテストコード上で後から行うのが特徴です。そのため、テスト対象のgetUserNameById関数を呼び出した後に、テストコードで呼び出し方の検証を行っています。

モック

モック

モックもスパイと同様に「テスト対象」から「依存コンポーネント」への出力(間接出力)を監視して記録します。

モックとスパイの違い

スパイは依存コンポーネントの呼び出し方(間接出力)の検証はテストコード上で後から行います

一方、モックは、間接出力の期待値(何回呼び出されるべきか、どのような引数で呼び出されるべきか等)を事前にセットし、その期待値がモックの呼び出し結果と一致するかどうかをモック内部で判断しています。そして、テストコードにはモック内部での検証結果を渡しています。

また、呼び出し対象も異なります。スパイは依存コンポーネント本体を呼び出しています。一方、モックは依存コンポーネントをモック化して模倣したものを呼び出しているため、依存コンポーネント本体を呼び出していません。

モックのプログラム例

以下にテストダブルとしてモックを使用しているプログラム例を示しています。

// ★テスト対象が依存しているdatabaseのgetNameメソッド
const database = {
    getName: function (id) {
        // このメソッドでは、データベースからデータを取得するが、このメソッドが未完成と仮定。
        // そのため、このメソッドでは何もしていない
    }
};

// ★テスト対象のgetUserNameById関数
function getUserNameById(id) {
    return database.getName(id);
}


// ★テストコード
const sinon = require('sinon');
const assert = require('assert');

describe('Mockのテスト', function () {
    it('正しいIDでdatabase.getNameを1度だけ呼び出しているか', function () {

        // 間接出力の期待結果をセット
        let expectedId = '123';

        // Sinon.JSを使ってdatabaseオブジェクトをモック化
        let mock = sinon.mock(database);

        // モックのdatabase.getメソッドが引数expectedId('123')で1度だけ呼び出されることを期待。
        // また、特定の値('Yamada Taro')を返すように設定。
        mock.expects('getName').once().withArgs(expectedId).returns('Yamada Taro');

        // テスト対象の関数を呼び出す
        getUserNameById('123');

        // モックの期待された挙動が満たされたかどうかを検証
        mock.verify();

        // モックの復元
        mock.restore();
    });
});

テスト対象のgetUserNameById関数はdatabasegetNameメソッドに依存しています。getNameメソッドは引数idの値を用いて、idに関するデータをデータベースから取得するメソッドですが、getNameメソッドは未完成と仮定します。

Sinon.JSのmockを用いて、依存コンポーネントであるdatabaseをモック化し、期待結果(databaseのgetNameメソッドが指定した引数で1回だけ呼び出されること)を設定しています。また、特定の値('Yamada Taro')を返すように設定しています。

テスト中にgetUserNameById関数を呼び出し、その結果を確認する際には、mock.verify()を使用してモックの期待された挙動が満たされたかどうかを検証しています。

このように、モックでは依存コンポーネントの呼び出し方(間接出力)の検証はモック内部で行うのが特徴です。また、モックは依存コンポーネントをモック化して模倣したものを呼び出しているため、依存コンポーネント本体を呼び出していません。

フェイク

フェイク

フェイクは本物の依存コンポーネントではないが、本物の依存コンポーネントと同じように動作する代替品です。

フェイクは本物の依存コンポーネントがテスト環境で使えない場合や、依存コンポーネントの応答が遅い場合などに使用します。例えば、サーバーにあるデータベースにアクセスしてそのデータを返すコンポーネントがあり、その応答が遅い場合、メモリ内の配列にアクセスしてそのデータを返すようなコンポーネントをフェイクとして作成します。

フェイクのプログラム例

以下にテストダブルとしてフェイクを使用しているプログラム例を示しています。

// ★テスト対象が依存しているdatabaseのgetNameメソッド
const database = {
    getName: function (id) {
        // このメソッドでは、データベースからデータを取得するが、このメソッドが未完成と仮定。
        // そのため、このメソッドでは何もしていない
    }
};

// ★テスト対象のgetUserNameById関数
function getUserNameById(id) {
    return database.getName(id);
}


// ★テストコード
const sinon = require('sinon');
const assert = require('assert');

describe('Fakeのテスト', function () {
    it('getUserNameByIdが正しい名前を返す', function () {

        let expectedId = '123';

        // Sinon.JSを使ってdatabase.getNameメソッドをフェイク化
        // このフェイク化されたメソッドは、どんな引数で呼び出されても'Yamada Taro'を返す。
        let fake = sinon.fake.returns('Yamada Taro');

        // 実際のdatabase.getNameメソッドはフェイクに置き換えられる。
        // そのため、テスト対象のgetUserNameById関数が呼び出されると、database.getNameメソッドの代わりにフェイクが使用される。
        sinon.replace(database, 'getName', fake);

        // テスト対象の関数を呼び出して、返り値をresultNameに保存
        let resultName = getUserNameById(expectedId);

        // 返された名前が期待した名前と同じであるかを検証
        assert.deepStrictEqual(resultName, 'Yamada Taro');

        // フェイクの復元
        sinon.restore();
    });
});

テスト対象のgetUserNameById関数はdatabasegetNameメソッドに依存しています。getNameメソッドは引数idの値を用いて、idに関するデータをデータベースから取得するメソッドですが、getNameメソッドは未完成と仮定します。

Sinon.JSのmockを用いて、依存コンポーネントdatabaseのgetNameメソッドをフェイク化しています。作成したフェイクは、どのような引数で呼び出されても一定の値(ここでは'Yamada Taro')を返すように設定されています。

sinon.replaceメソッドを使って、実際のdatabase.getNameメソッドを作成したフェイクに置き換えています。この結果、テスト対象であるgetUserNameById関数が呼び出されると、database.getNameメソッドの代わりにフェイクが使用されます。

そして、テスト対象の関数を呼び出し、その戻り値が期待した値と一致することを検証しています。これにより、テスト対象がフェイクを適切に使用していることが確認できます。

ダミー

ダミーは実際のテスト本体には使用しないが、「引数の都合上」や「コンパイルの都合上」などで用意しないといけないものです。ダミーはテスト対象の結果に影響を与えません。

ダミーのプログラム例

以下にテストダブルとしてダミーを使用しているプログラム例を示しています。

// ★テスト対象のgetFullNameメソッド
class User {
    constructor(firstName, lastName, address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.address = address;
    }
    getFullName() {
        return `${this.lastName} ${this.firstName}`;
    }
}

// ★テストコード
const assert = require('assert');
const sinon = require('sinon');

describe('Dummyのテスト', function () {
    it('名前がTaro、苗字がYamadaの時にYamada Taroを返す', function () {
        // ダミーデータを設定。インスタンス化する際に必要となるため。
        const dummyAddress = 'Address';
        // Userクラスをインスタンス化。
        const user = new User('Taro', 'Yamada', dummyAddress);
        // getFullNameメソッドの返り値をfullNameに代入
        const fullName = user.getFullName();
        // getFullNameメソッドの結果が正しいことを検証します(ダミーデータが結果に影響を与えないことが分かります)。
        assert.strictEqual(fullName, 'Yamada Taro');
    });
});

ここでは、Userクラスのインスタンスを作成するためにダミーデータ(dummyAddress)を使用しています。このUserクラスは、コンストラクタでfirstNamelastNameaddressを必要とします。しかし、テストではgetFullNameメソッドの挙動のみを検証していて、addressは関係ありません。

そのため、addressには実際の値ではなくダミーデータ(Address)を使用します。これにより、テストの主な目的であるgetFullNameメソッドの挙動を確認しながらも、Userクラスのインスタンスを適切に作成することができます。

補足

各テストダブル(スタブ、スパイ、モック、フェイク、ダミー)の意味については、多少曖昧であり、異なるテストフレームワークやライブラリでは異なる意味を持つことがあります。

本記事のまとめ

この記事では『テストダブル』について、以下の内容を説明しました。

  • テストダブルとは
  • テストダブルの種類・分類
    • スタブの特徴とプログラム例
    • スパイの特徴とプログラム例
    • モックの特徴とプログラム例
    • フェイクの特徴とプログラム例
    • ダミーの特徴とプログラム例

お読み頂きありがとうございました。