テスト自動化

テストダブルの種類と使い分け

テストダブルとは?

テストダブル(Test Double)とは、テストを実行する際に実際のオブジェクトの代わりに使用するオブジェクトのことです。

ソフトウェアテストでは、実行環境やシステムの制約によって思うようにテストができない場合があります。

例えば以下のようなケースでは、実際のオブジェクトを使うことが難しくなります。

  • 顧客のデータベースを勝手に変更してはいけない
  • 実行コストが高く、頻繁にテストできない
  • 外部のAPIを使用しており、動作を自由にコントロールできない

このような場合、テストダブル(Test Double)を活用することで、安全かつ柔軟なテストが可能になります。

またテストの目的によっては、特定の動作を意図的に制御したい場合もあります。例えば、

  • 特定の外部メソッドが例外をスローする状況をシミュレートし、エラーハンドリングが正しく機能するかを確認する
  • 外部メソッドが想定通りに呼び出されたかを検証する

こうしたケースでは、テストダブルを活用することで、自由に動作をカスタマイズしながらテストを行うことが可能になります。

JavaScriptのテストではSinonというライブラリを用いてテストダブルを用意することが多いです。

テストダブルには、用途に応じて以下の5種類があります。

種類目的
Dummy(ダミー)必要だが使われない引数を埋める
Stub(スタブ)関数の返り値を固定する
Spy(スパイ)関数の呼び出し状況を記録する
Mock(モック)事前に期待値を設定し、厳密に検証する
Fake(フェイク)実際の実装を代替する

Dummyとは?

Dummy(ダミー) は、テストダブルの一種でテストの実行に必要だけれども実際には使用されないオブジェクトのことを指します。

下記の特徴を持ちます。

  • 使われることが前提ではなく、ただの「空の値」や「適当なオブジェクト」
  • 他のテストダブル(Spy, Stub, Mock, Fake)とは異なり、動作の記録や制御を行わない
  • テストコードの引数を埋めるためだけに使用する

簡易な具体例は下記です。

ログを記録するメソッドを持つが、当該メソッドがテストでは不要な場合、Dummyを作成してテストの実行を進めます。

class Logger {
  log(message) {
    console.log(message); // 本番環境ではログを記録する
  }
}

class UserService {
  constructor(logger) {
    this.logger = logger;
  }

  createUser(name) {
    this.logger.log(`User ${name} created`);
    return `User ${name} created`;
  }
}

// テスト時は logger を使用しないため、ダミーを渡す
const dummyLogger = { log: () => {} }; // 何もしないメソッド
const userService = new UserService(dummyLogger);

console.log(userService.createUser("Alice"));  
// → "User Alice created"(ダミーロガーなので実際のログ出力なし)

Spyとは?

Spy(スパイ)は、テストダブルの一種で関数やメソッドが呼び出されたか、どのような引数で呼び出されたかを追跡するために使用するオブジェクトです。

スパイは関数やメソッドの呼び出し回数や引数、返り値などを「監視」して記録しますが、元の関数の動作を変更しません。

特徴は下記です。

  • 呼び出し回数引数返り値を監視できる。
  • 監視するだけでなく、元の関数の動作もそのままにしておく(関数を呼び出しても、元の処理が実行される)。
  • 主に関数が正しく呼ばれているか、引数が正しいかなどのテストで使用される。

簡易な具体例は下記です。

関数が正しく呼ばれたかチェックするためにSpyを使っています。

import sinon from 'sinon';
import { expect } from 'chai';

// テスト対象の関数
function greet(name) {
  console.log(`Hello, ${name}!`);
}

describe('greet function', () => {
  it('should be called with the correct argument', () => {
    const spy = sinon.spy(greet); // greet 関数にスパイを設定

    greet('Alice'); // 実行

    // greet 関数が正しい引数で呼ばれたかチェック
    expect(spy.calledWith('Alice')).to.be.true;

    // スパイをリストア
    spy.restore();
  });
});

Stubとは?

Stub(スタブ)はテストダブルの一種で、テスト対象のコードの一部を置き換えて決められた返り値を返すように設定することができます。

スタブを使うと実際の依存関係(外部のAPI、データベース、ファイルシステムなど)を呼び出さずに、テストが高速に、また独立して実行できるようになります。

簡易な具体例は下記です。

外部APIを呼び出す関数をテストする場合、実際にAPIリクエストを行うと遅延や失敗する可能性があるため、スタブを使ってAPI呼び出しを模倣します。

import sinon from 'sinon';
import { expect } from 'chai';

// 外部APIを呼び出す関数
function fetchData(apiClient) {
  return apiClient.get('https://example.com/data');
}

describe('fetchData function', () => {
  it('should return mock data', () => {
    const stub = sinon.stub(); // スタブ作成
    stub.withArgs('https://example.com/data').returns({ success: true }); // 引数に応じて返り値を設定

    const result = fetchData({ get: stub }); // APIクライアントをスタブで置き換える

    // 返り値を検証
    expect(result).to.deep.equal({ success: true });
    expect(stub.calledOnce).to.be.true; // スタブが1回だけ呼ばれたことを確認
  });
});

Mockとは?

Mock(モック)はテストダブルの一種で、特定の方法で動作をシミュレートし、呼び出しの振る舞いや結果を検証することに使われます。

SpyとMockはよく似ていますが、Spyは特定の処理の出力を保持するだけで出力結果の評価はテストコードで別途行うのに対し、MockはMock内で出力結果の評価まで実施します。

またSpyは実際の処理を実行するのに対して、Mockは実際の処理を模擬してテストを進めます。

「顧客のデータベースを勝手に変更してはいけない」「実行コストが高く、頻繁にテストできない」などの理由で実際の処理を実行したくない場合は、Mockを使うと良いでしょう。

簡易な具体例は下記です。

外部APIを呼び出す関数をテストする場合に、モックを使用してAPIが正しく呼び出されるかを検証しています。

import sinon from 'sinon';
import { expect } from 'chai';

// APIを呼び出す関数
function fetchData(apiClient) {
  return apiClient.get('https://example.com/data');
}

describe('fetchData function', () => {
  it('should call the API once with the correct URL', () => {
    const mock = sinon.mock(); // モック作成
    mock.expects('get').once().withArgs('https://example.com/data').returns({ success: true }); // 期待する呼び出しと引数設定

    const result = fetchData({ get: mock.get }); // APIクライアントをモックで置き換える

    // モックの振る舞いを検証
    mock.verify(); // モックが期待通りに呼ばれたかを検証

    expect(result).to.deep.equal({ success: true }); // 結果を検証
  });
});

Fakeとは?

Fake(フェイク)はテストダブルの一種で、実際の実装に似た簡易的な実装を持つオブジェクトです。

例えば、テストのために簡易的なメモリ内データベースを作成し、実際のデータベースの代わりに使います。

このフェイクデータベースは、テスト対象のコードがデータベース操作を正しく行うかどうかを確認するために使用されます。

まとめ

再掲になりますが、テストダブルの種類と目的は下記の通りです。

種類目的
Dummy(ダミー)必要だが使われない引数を埋める
Stub(スタブ)関数の返り値を固定する
Spy(スパイ)関数の呼び出し状況を記録する
Mock(モック)事前に期待値を設定し、厳密に検証する
Fake(フェイク)実際の実装を代替する

目的に応じて適切なテストダブルを選択することが重要です。