【Laravel】モックを使ってテストを書く

Mockeryの基本的な使い方のメモと、自分が実際に書いたモックを利用したテストを載せておきます。今回は外部APIの呼び出しをモックしています。

###なぜモックするのか
アプリケーションにはデータベースや外部APIのように外部との連携がある場合があります。その場合、外部の要素の状態によって、アプリの振る舞いは変わっていきます。テストの際、当然それらに何かしらのトラブルが発生すれば、テストは失敗となります。
こういったテストが外部に「依存」している状態を避けるために、外部の要素を扱うクラスの振る舞いを、あらかじめ定義したものにすり替える必要があります。これがテストにおける、モックの役割です。
LaravelではPHPのモックのフレームワークであるMockeyを利用すると簡単にモックを利用できます。

公式リファレンスを見てみましょう
[Laravel 5.8 モック](https://readouble.com/laravel/5.8/ja/mocking.html)
[Mockery1.0 Mockery](https://readouble.com/mockery/1.0/ja/index.html#%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9)

##テストの例(外部APIの呼び出しがある場合)

###テストの対象となるコントローラ
```php:MoviesController.php
<?php

namespace App\Http\Controllers;

use App\Models\Movie;
use App\Libraries\TmdbService; //外部APIを呼び出すクラス

class MoviesController extends Controller
{
    public function store(Request $request, Movie $movie, $tmdb_movie_id)
    {
        $movieService = new TmdbService;

        //APIからデータを受け取り、配列を返すメソッド
        $movie_array = $movieService->getMovieArray($tmdb_movie_id);

        //データベースに保存 
        $movie->storeMovie($movie_array); 
    }
}
```
テストするのは`MoviesController`の`store`アクションで、APIのパラメータをリクエスト->APIからのデータを受け取る->DBへ保存という流れで動作します。
ここでは外部APIを呼び出す`App\Libraries\TmdbService`クラスの`getMovieArray()`メソッドが使われます。テストの際は外部APIに依存させないためにこのクラスをモックします。

###テストコード
テスト対象：MoviesControllerのstore()
モック対象：App\Libraries\TmdbServiceクラス

```php:StoreTest.php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use App\Models\User;
use App\Models\Movie;

class StoreTest extends TestCase
{
    use RefreshDatabase;

    /**
     * @dataProvider RequestData
     * @param $movie
     */
    public function testStoreMovie($movie)
    {
        $tmdbServiceMock = Mockery::mock('overload:\App\Libraries\TmdbService');
        $tmdbServiceMock->shouldReceive('getMovieArray')
                         ->once()
                         ->andReturn($movie);

        $user = factory(User::class)->create();
        $url = url('/movies/157336/store');
        $response = $this->actingAs($user)->get($url);

        $this->assertDatabaseHas('movies', [
            'tmdb_id'      => $movie['id'],
            'title'        => $movie['title'],
            'release_date' => $movie['release_date'],
            'runtime'      => $movie['runtime'],
            'poster_path'  => $movie['poster_path'],
            'tagline'      => $movie['tagline'],
            'overview'     => $movie['overview']
        ]);
    }

    //データプロバイダ  
    public function RequestData()
    {
        return [
            'movieData' => [
                'id'           => 157336,
                'title'        => 'インターステラー',
                'release_date' => '2014-11-05',
                'runtime'      => 169,
                'poster_path'  => '/v6oNcydMvHwV8sxNIF8eivbw8tK.jpg',
                'tagline'      => '必ず、帰ってくる。 それは宇宙を超えた父娘の約束━━。',
                'overview'     => '近未来の地球では植物の枯死、異常気象により人類は滅亡の危機に立たされていた...',
                'genres' => [
                    0 => ['id' => 12], 
                    1 => ['id' => 18], 
                    2 => ['id' => 878]
                ]
            ]          
        ];
    }
}
```

##Mockeryメモ
モックの生成は以下の部分で行われています。

```php:StoreTest.php
$tmdbServiceMock = Mockery::mock('overload:\App\Libraries\TmdbService');
$tmdbServiceMock->shouldReceive('getMovieArray')
                 ->once()
                 ->andReturn($movie);
```
###テストダブルの作成
[テストダブル作成](https://readouble.com/mockery/1.0/ja/creating_test_doubles.html)

```php
$tmdbServiceMock = Mockery::mock('overload:\App\Libraries\TmdbService');
```
- `mock()`メソッドでモックオブジェクトを生成します。
今回の例では、テスト対象の関数内でApp\Libraries\TmdbServiceのインスタンスがnewされるため、インスタンス生成時にモックオブジェクトにすり替える必要があります。
また今回のようにモックするクラスと依存が強い場合、`overload`をプレフィクスとしてつけます。
[強い依存のモック](https://readouble.com/mockery/1.0/ja/mocking_hard_dependencies.html?header=)

###エクスペクションの宣言
[エクスペクション](https://readouble.com/mockery/1.0/ja/expectations.html)
モックを生成したらその振る舞いを定義します。要は「このメソッドは何回呼ばれ、呼ばれた場合にこの値を返す」という要領で期待する動きを設定します。

```php
$tmdbServiceMock->shouldReceive('getMovieArray')
                 ->once()
                 ->andReturn($movie);
```
- メソッド呼び出しのエクスペクションの宣言には`shouldReceive()`メソッドを使います。
今回のテストではgetMovieArray()というメソッドが呼ばれることを期待しています。

- `once()`メソッドは呼び出し回数のエクスペクションを宣言します。`times($n)`で回数をします。また一回なら`once()`、二回なら`twice()`とすることもできます。
今回のテストではgetMovieArray()が一回だけ呼ばれます。

- 戻り値のエクスペクション宣言には`andReturn()`メソッドを使用します。
今回はデータプロバイダで戻り値を設定しています。

###あとがき
調べていて思ったのがモックに関する情報は易しいものが少なく、その上使い方も一様ではないため公式を参照するのが最良ということです。モックに限った話ではないですが。