CakePHP3でsnapshot testを書く

# はじめに
テストコードが無い箇所にリグレッションテストを書きたい時やテストを書く時間が取れないけど、最低限アウトプット内容は保証したい時ってありますよね。そんなときに便利なsnapshot testingと言うものをご紹介いたします。

# snapshot testingとは
もともとは、Facebook製のJSテストフレームワーク[Jest](https://facebook.github.io/jest/docs/ja/snapshot-testing.html)の機能として提供されているものです。

また、[PHPでsnapshot testingをやりませんか？](https://qiita.com/Khigashiguchi/items/1c50105aff2c8acf45d5)という記事で、snapshot testingについてご紹介させていただきました。詳細はこちらをご参照下さい。

概要を言うと、最初のテスト実行時にテストケースのアウトプットが保存（スナップショット）され、それを目視で確認して期待通りなことを確認します。
2回目以降はアウトプットとスナップショットを比較して一致していればテストが通るようになります。

今回は、Jsonを返すControllerアクションのテストを考えます。

# 準備
snapshot testingのメソッドをphpunitで実行するためのライブラリ。[spatie/phpunit-snapshot-assertions](https://github.com/spatie/phpunit-snapshot-assertions)を使います。

- composer install

```bash
$ composer require --dev spatie/phpunit-snapshot-assertions
```

- テストクラスに読み込み

```php
<?php
namespace App\Test\TestCase\Controller;

use App\Controller\SampleController;
use Cake\TestSuite\IntegrationTestCase;
+ use Spatie\Snapshots\MatchesSnapshots;
+ 
class SampleControllerTest extends IntegrationTestCase
{
  + use MatchesSnapshots;
  // ...
}
```

# 書き方
例として、下記の2パターンを持つテストケースを書きます。
- METHOD: `GET`だった場合は、エラーメッセージを返す
- バリデーションエラーが有った場合は、当該箇所のエラーと共にエラーメッセージを返す

```php
public function testIndexError()
{
  // METHOD: `GET`だった場合は、エラーメッセージを返す
  $this->get('/sample');
  $this->assertMatchesJsonSnapshot($this->_getBodyAsString());

  // バリデーションエラーが有った場合は、当該箇所のエラーと共にエラーメッセージを返す
   $data = [
    'sample' => [
      [
        'param1' => null,           
        'param2' => null,
        'param3' => null, 
      ]
    ]
  ];
  $this->post('/sample', $data);
  $this->assertMatchesJsonSnapshot($this->_getBodyAsString());
}
```

`assertMatchesJsonSnapshot`というメソッドの引数に、レスポンスを渡してあげます。
CakePHP3系でテストケース内でレスポンスを取得するには、`$this->_getBodyAsString()`を使用します。

実行してみます。

```bash
% vendor/bin/phpunit
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

I 

Time: 10.73 seconds, Memory: 22.00MB

OK, but incomplete, skipped, or risky tests!
```

初回実行時は、アサーションはスキップされていますが、`tests/Controller/__snapshots__`というディレクトリが作られ、そこにJSONレスポンスがスナップショットとして保存されています。
下記2点のファイルが作られます。

- `SampleControllerTest__testIndexError__1.json`

```json
{
    "message": "Method Not Allowed",
}
```

- `SampleControllerTest__testIndexError__2.json`

```json
{
    "message": {
        "sample": [
            {
                "param1": {
                    "This field cannot be left empty"
                },
                "param2": {
                    "This field cannot be left empty"
                },
                "param3": {
                    "This field cannot be left empty"
                }
            }
        ]
    },
```

snapshot testingという名前の理由は、初回実行時に結果のsnapshotを取ることにあります。最初の結果があっていれば次回実行時は、snapshotファイルとの比較を行われます。

```bash
% vendor/bin/phpunit
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

　. 

Time: 10.73 seconds, Memory: 22.00MB

OK (1 tests, 2 assertions)
```

# dataProviderを使ったテストを書いた場合
dataProviderで複数のアサーションを行う場合は、スナップショット名が少しわかりやすくなります。

```php
public function IndexErrorProvider()
{
  return [
    'not allowed get request' => [
      'get',
       null
     ], 
     'invalid parameter post' => [
        'post',
        [
          'sample' => [
            [
              'param1' => null,
              'param2' => null,  
              'param3' => null,
            ]
          ]
        ],
     ],
  ];
}

public function testIndexError($method, $data)
{
  $this->$method('/sample', $data);
  $this->assertMatchesJsonSnapshot($this->_getBodyAsString());
}
```

これを実行した際は、スナップショット名が下記のようになります。
- `SampleControllerTest__testIndexError__ with data set "not allowed get request"__1.json`
- `SampleControllerTest__testIndexError__ with data set "invalid parameter post"__1.json`

スナップショットファイル名に、dataProviderのレコード名がつきます。

## その他
その他、スナップショットファイルの更新などは、[PHPでsnapshot testingをやりませんか？](https://qiita.com/Khigashiguchi/items/1c50105aff2c8acf45d5)をご参照下さい。

# 最後に
テストを書きたいけど、なかなか書く時間が取れないという方はぜひ使ってみてはいかがでしょうか？