🐡

PHPUnitでスタブとモックを理解する!【テストダブル】

2022/05/12に公開

はじめに

なんとなく言葉だけ使っている「スタブとモック」。
「ただスタブとモックって何?」と言われると言語化できない、、、。
今回は、実際にPHPUnitでテストコードを書いてみて、スタブとモックの違いについて理解していきたいと思います。

ソースはこちら
https://github.com/shun57/phpunit-docker

実行環境

PHP 8.0
PHPUnit 9.5

※実際に触ってみたい方はこちらの記事で実行環境を作成してください。
https://zenn.dev/shun57/articles/4b2cbf33255de4

PHPUnitのドキュメントをみると

PHPUnitのドキュメントを確認すると、8.テストダブルという章があり、そこでスタブとモックの説明がされています。
つまり、スタブもモックもテストダブルのひとつのようです。

テストダブル・・・?
聞きなれない言葉ですね、まずこの言葉から調べてみます。

https://phpunit.readthedocs.io/ja/latest/test-doubles.html

テストダブルとは?

まずWikipediaを見てみると以下のような記載があります。
依存コンポーネントのダミーとなる、いわゆるスタブとモックのイメージのまんまですね。
さらに詳しく調べてみます。

テストダブル (Test Double) とは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと。
「テストダブル」(2022年5月8日 (日) 18:00)『ウィキペディア日本語版』。https://ja.wikipedia.org/wiki/テストダブル

なぜテストダブルが必要か?

例えば依存しているコンポーネントが、外部決済を伴うものやDB操作が必要な場合、テストで実際に実行しづらいですよね。
その場合、テスト実行時にこちらで用意した代替コンポーネントに置き換えられれば便利です。
その代替コンポーネントを「テストダブル」と称しているようです。

xUnit PatternsのTest Doubleパターン

PHPUnitのドキュメント8.テストダブルを読むと、最初にGerard Meszaros氏の引用文章があります。
このGerard Meszaros氏のサイトをみてみると、以下5つのテストダブルパターンが挙げられています。

  1. ダミーオブジェクト
    テスト対象に影響を与えないコンポーネントを代替するテストダブル
  2. テストスタブ
  3. テストスパイ
    スタブとモックの機能を持つテストダブル
  4. モックオブジェクト
  5. フェイクオブジェクト
    本コンポーネントと同じように動作するテストダブル

※ スタブとモック以外のテストダブルについては説明は割愛しますので、気になる方は上記サイトをご参照ください。

つまり、スタブとモックはテストダブルパターンのひとつであり、依存コンポーネントを置き換えるための手法のひとつであることがわかりました。

そもそも依存とは?(わからない方向け)

スタブとモックを理解する上で、そもそも依存について理解しておく必要がありますので、触れておきます。
※ 損なん当たり前にわかるぜ!って方は読み飛ばしてください。

依存

あるコンポーネントAを動かすために、他のコンポーネントBが必要であることを、AはBに依存している(依存関係にある)といいます。

具体例を見てみましょう。
以下のコンポーネントに依存関係はあるでしょうか?

function sumOfArraySomeTimesZero(array $nums, CheckNumsClass $check_nums_obj): int
{
    try {
        // ランダムでエラーを返すメソッド
        $check_nums_obj->somtimesError();
        return array_sum($nums);
    } catch (Throwable $e) {
        return 0;
    }
}

答えは、依存関係があります。
sumOfArraySomeTimesZero関数の出力が、sometimesErrorメソッドの出力に左右されているからです。
実行するたびに、sometimesErrorがエラーを返す可能性があり、エラーが返された場合は0を返します。

例えば、このロジックを単体テストしようとするとどうでしょうか?
出力が定まらないので、全てのテストケースを網羅できないですよね?
[1, 2]を与えたときに3が出力されるのか、0が出力されるのかランダムだからです。

この依存コンポーネントに左右されずに単体テストを実施するために、テストダブルパターンを使います。

それでは本題に入りましょう。

テストスタブとは?

テスト対象の依存コンポーネントを置き換えて、都合の良い任意の値を返すようにするテストダブルのことです。
テスト対象が依存コンポーネントの出力に左右されずに意図した出力ができるかどうかをテストするために利用します。
つまり、テスト対象が意図通りに動くかどうか? をテストするためのテストダブルをスタブと言います。

PHPUnitでスタブ

依存の説明の際のメソッドをスタブを用いてテストしてみます。
ランダムでエラーを返すsomtimesErrorメソッドの返り値を操作できたらテストができますよね?
そこでスタブを使います。

テスト対象

function sumOfArraySomeTimesZero(array $nums, CheckNumsIF $check_nums_obj): int
{
    try {
        // ランダムでエラーを返すメソッド
        $check_nums_obj->somtimesError();
        return array_sum($nums);
    } catch (Throwable $e) {
        return 0;
    }
}

テストメソッド① 配列の値を足して返す

まずはちゃんと配列の数値を足した値を返すかをテストします。
その場合、somtimesErrorがエラーを返さないように設定します。

PHPUnitでスタブを作るのは簡単で、createStub()を使い、引数にスタブにしたい対象クラスを渡します。

$stub = $this->createStub(CheckNums::class);

スタブは任意の値を返すことができます。
PHPUnitではmethod()を使い、引数に操作したいメソッドを渡します。
willReturn()を使うと返したい値を設定できますが、今回は何も返しません。

$stub->method('somtimesError');

最終的には以下のテストコードにしました。
somtimesErrorは何も返さないため、必ず数値を足した値を返すようになります。

public function testSumOfArrayWithStub(): void
{
    $nums = [1, 2];
    // CheckNumsクラスのスタブを作る
    $stub = $this->createStub(CheckNums::class);
    // 何も返さないスタブの設定を行う
    $stub->method('somtimesError');
    // テスト対象のメソッドを実施すると、スタブを返します。
    $result = sumOfArraySomeTimesZero($nums, $stub);
    $this->assertSame($result, 3);
}

テストメソッド② 0を返す

テスト対象はsomtimesErrorがエラーを返したときに0を返します。
そのため、スタブでエラーを発生させるようにします。

同様にcreateStubを使い、エラーを返すようにしましょう。
PHPUnitでは以下のように記載すれば例外を発生させることができます。

$stub = $this->createStub(CheckNums::class);
// 例外を出力するスタブの設定を行う
$stub->method('somtimesError')
    ->will($this->throwException(new Exception));

これで必ず例外が発生しますので、全体のソースとしては以下のようになります。

public function testSomeTimesZeroWithStub(): void
{
    $nums = [1, 2];
    // CheckNumsクラスのスタブを作る
    $stub = $this->createStub(CheckNums::class);
    // 例外を出力するスタブの設定を行う
    $stub->method('somtimesError')
        ->will($this->throwException(new Exception));

    $result = sumOfArraySomeTimesZero($nums, $stub);
    $this->assertSame($result, 0);
}

モックオブジェクトとは?

テスト対象の依存コンポーネントを置き換えて、そのコンポーネントが正しく呼び出されているかを検証するために用意するテストダブルのことです。
実際にテスト対象の依存コンポーネントを実行せずに、呼び出した回数などをテストするために利用します。
つまり、テスト対象の依存コンポーネントが意図通りに動くか? をテストするためのテストダブルをモックと言います。

PHPUnitでモック

APIを使用するメソッドをモックを用いてテストしてみます。

テスト対象

対象のAPIを実行するメソッドです。
いい機会なので、ランダムなユーザーデータを生成してくれる無料のオープンソースAPIを利用させていただきました。
※実際は「ツイートする」などAPIの方がモックの説明には適していたかも、、、

https://randomuser.me/

function getUserData(UserApiService $userApiService): void
{
    $url = "https://randomuser.me/api/";
    // APIの実行を行うメソッド
    $userApiService->curl_test($url);
    // なんらかの処理など
}

テストメソッド

テストのたびに毎回API通信が発生するのは嫌ですよね?
そこでモックを使います。
テストでは、
・APIが実際に1回だけ実行されているか?(2回呼ばれてたらまずい)
・引数に適切なurlが指定されているか?(タイポなどのミス)
を確認します。

PHPUnitでモックを作るのも簡単で、createMock()を使い、引数にモックにしたい対象クラスを渡します。

$mock = $this->createMock(UserApiService::class);

これで代替コンポーネント$mockが出来ましたので、実際にAPIは実行しません。
次にモックの確認をします。

$url = 'https://randomuser.me/api/';
$mock->expects($this->once()) // 1度だけ呼ばれるか
    ->method('curl_test') // 対象メソッド
    ->with($url); // 引数が指定の値になっているか

最後に、モックを渡して、テスト対象のメソッドを実行します。
全体のコードは以下です。

public function testGetUserData(): void
{
    $url = 'https://randomuser.me/api/';
    // UserApiServiceクラスのモックを作る
    $mock = $this->createMock(UserApiService::class);

    $mock->expects($this->once()) // 1度だけ呼ばれるか
        ->method('curl_test') // 対象メソッド
        ->with($url); // 引数が指定の値になっているか

    getUserData($mock);
}

まとめ

  • モックもスタブもテストダブルの一つで、テスト時に依存コンポーネントを置き換える代替コンポーネントの役割を果たす
  • スタブは、コンポーネントを置き換えて、都合の良い任意の値を返すようにするもの。依存に左右されずにテスト対象メソッドが動くかどうかをテストするときに使う。
  • モックは、コンポーネントを置き換えて、正しく呼び出しができているか確認できるようにするもの。依存コンポーネントを実際に実行せずに、呼び出し回数や引数が意図通りか確認するときに使う。
GitHubで編集を提案

Discussion