LaravelでMockeryを使った試験の話

サービスを試験する時、試験対象サービスから別の連結するサービスがあったとします。
その時はどう試験しますか?
以降の連なるサービスを含めて試験を行うというのは変ですね。
Unit試験は自己完結が範囲だとか愚行します。
以降のサービスに不具合があった場合にはこのサービスの試験がNGになるのは変です。
つまりは以降のサービスをブラックボックスにできる方法がベストです。
次のサービスに引き渡す値の結果が想定した結果を戻るだけでいいのです。

これを解決するのにMockery(mock)を使います。

mockとは

mockとは… 辞書で調べるとこんな感じです。

まがいの、偽りの、まねごとの

https://ejje.weblio.jp/content/mock

mockオブジェクトとは本来のオブジェクトではなく想定の結果を返すオブジェクトということです。

Mockeryとは

Mockery は、PHPUnit や PHPSpec などのテストフレームワークで使用するための、 シンプルかつ柔軟な PHP モックオブジェクトフレームワークです。

https://github.com/mockery/mockery

試験対象とmockの概略

今回の試験対象のBooksServiceは著者情報を取得するためにSearchAuthorServiceと連携します。

今回はこのSearchAuthorServiceをmockにしてBooksServiceを試験します

SearchAuthorServiceではメソッド searchに書籍名を渡せば著者名を返してくれます
というインタフェイスを定義されています。

SearchAuthorService mockでは以下の特徴を持っています

  • SearchAuthorServiceクラスの型をもったオブジェクト
  • searchメソッドに書籍名を渡せば決められた著者情報を返してくれる

これによりBooksServiceでは、SearchAuthorServiceは想定した動作することで試験は可能になります。

今回の試験対象部分ですが、SearchAuthorServiceのsearchが該当箇所です

        try {
            // 著者情報を取得します
            $currentBook->author = $this->authorService->search($book->title);
        } catch (Exception $e) {
            Log::debug($e);
        }

試験対象ソース : BooksService.php

PHPUnitでmockを作る方法

mockをPHPUnitに実装するには以下の構成となっております

  • mockインスタンスを作る
  • mockメソッドの結果値を指定する
  • たまに、mockメソッドの例外を発生させる

mockを含んだ試験の内容を抜粋します

        $authorService = Mockery::mock(SearchAuthorService::class);
        $authorService
            ->shouldReceive('search')
            ->with('ドメイン特化言語 パターンで学ぶDSLのベストプラクティス46項目')
            ->andReturn('マーチン ファウラー');
        $authorService
            ->shouldReceive('search')
            ->with('3月のライオン(13)')
            ->andReturn('羽海野チカ');
        $authorService
            ->shouldReceive('search')
            ->andReturn('不明');

        $booksService = new BooksService($authorService);

        $book = $booksService->serve(1 , false);
        $this->assertEquals('ドメイン特化言語 パターンで学ぶDSLのベストプラクティス46項目' , $book->title);
        $this->assertEquals('マーチン ファウラー', $book->author);

試験全ソース : BooksServiceTest.php

mockのインスタンス作成方法

mockのインスタンスを作成するには普通とは異なります。

本来はこの様にインスタンスを作成してサービスに引き継ぎます。

        $authorService = new SearchAuthorService();
        $booksService = new BooksService($authorService);

mockの場合はMockeryを使ってmockのクラスを作ります。
それを連携させます。

        $authorService = Mockery::mock(SearchAuthorService::class);
        $booksService = new BooksService($authorService);

mockのメソッドの結果を指定する

次にどのメソッドから何を返すのかを定義します

        $authorService = Mockery::mock(SearchAuthorService::class);
        $authorService
            ->shouldReceive('search')
            ->with('ドメイン特化言語 パターンで学ぶDSLのベストプラクティス46項目')
            ->andReturn('マーチン ファウラー');

        $booksService = new BooksService($authorService);

shouldReceiveで指定したメソッド (search)の引数(with)の場合に何を返すか(andReturn)をそれぞれ定義しています。
引数(with)がない場合は、そのメソッドの全ての結果になります

mock処理で例外を発生させる

メソッドを利用した時に例外を発生させることもできます。

        $authorService
            ->shouldReceive('search')
            ->andThrow(new Exception('不明'));

これにより例外処理の試験が楽にできますので結構便利です。

自己満足的にカバレッジレポートが100%近くできますw

例外部分がカバーできる

ちなみに

ここまで来てネタバラシですが、今回のSearchAuthorServiceのソースを出しますと…

<?php

namespace App\Services;

use Exception;

/**
 * 著者情報取得サービス
 */
class SearchAuthorService
{
    /**
     * 書籍タイトルから著者を検索します
     * @param string $title 書籍タイトル
     * @return string|null 著者名
     * @throws Exception 何らかの例外
     */
    public function search(string $title) : ?string {
        return null;
    }
}

全ソース : SearchAuthorService.php

なにもできていませんが、BooksServiceは想定した試験ができます。

終いに

今回はLaravelを使ったサービスの連携でMockeryを使ってmock試験の方法を書きました。

前回記事のSeederと合わせればかなりの試験をカバーすることができます。

ついでに今回のmock試験を行うということはSearchAuthorServiceにおいて以下の試験は必ず通ることが前提になります。試験一覧に追加しておきましょう。

        $authorService = new SearchAuthorService();
        $this->assert('マーチン ファウラー',$authorService->search('ドメイン特化言語 パターンで学ぶDSLのベストプラクティス46項目'));
        $this->assert('羽海野チカ',$authorService->search('3月のライオン(13)'));

あとは… HTTPリクエストとかを隠蔽する方法があればな…

        $contents = @file_get_contents($url, false, stream_context_create($options));

これを!独立させずに何とかしたいな… w

ちなみに独立させると…

class HTTPAccessService
{
    public function access(string $url , array $options) : ?string {
        return @file_get_contents($url, false, stream_context_create($options));
    }
}

これかな…

参照

プロジェクト : https://github.com/wataru775/learning-leravel/tree/20220427_mock_test