Laravel Storageの接続失敗をphpunitでテスト

Laravel fail test for storage on phpunit PHP
Laravel fail test for storage on phpunit

Laravelで外部ストレージ等にアクセスする際、Storageを使ったりしますよね。
今回は、Storageで接続するサーバとのFTP接続に失敗したら
リトライする機能をテストしてみたいと思います。

環境

Windows 10
Laravel 6.x

リトライ機能の実装

↓こんな感じで組んでおきます。

class StorageRetrySample
{
  public function retryMakeDirectory($path)
  {
    $isSuccess = false;
    for ($cnt = 0; $cnt < config('const.retry.limit'); $cnt++) {
      try {
        Storage::makeDirectory($path);
        $isSuccess = true;
        break;
      } catch (\Exception $e) {
        Log::debug('retry ... ' . $cnt);
        usleep('const.retry.sleep_time');
      }
    }
    if (!$isSuccess) throw $e;
    return 'success';
  }
}

retry_limitsleep_timeはconst.phpで定義しておきましょう。
今回のサンプルでは以下の設定にします。

return [
  'retry' => [
    'limit' => 5,
    'sleep_time' => 100000
  ]
]

テスト作成

今回の狙いは「接続に失敗した場合にリトライする」なので、
想定する例外はConnectionRuntimeExceptionです。
これが発生した時にリトライ出来ていればOK。

namespace Tests\Unit;

use League\Flysystem\ConnectionRuntimeException;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
use StorageRetrySample;

class StorageRetrySampleTest extends TestCase
  // このテストはエラーが発生することを期待してます
  public function testRetryMakeDirectoryGetException()
  {
    // 接続失敗のエラーを待つ
    $this->expectException(ConnectionRuntimeException::class);
    // makeDirectoryメソッドが呼ばれたら5回エラーを投げる
    Storage::shouldReceive('makeDirectory')
      ->atMost()
      ->times(5)
      ->andThrow(new ConnectionRuntimeException('接続失敗'));
    // makeDirectoryメソッドが呼ばれたら1回結果をそのまま返す
    Storage::shouldReceive('makeDirectory')
      ->once()
      ->andReturnSelf();
    // 実際に動かすと、↑で準備していたものが動き出す
    StorageRetrySample::retryMakeDirectory('test');
  }

  // このテストはエラーが発生しないことを期待してます
  public function testStorageMakeDirectoryRetrySafe()
  {
    // エラーが返ってくることを期待しないので、何も書かない
    // 4回エラーを返す
    Storage::shouldReceive('makeDirectory')
      ->atMost()
      ->times(4)
      ->andThrow(new ConnectionRuntimeException('接続失敗'));
    // 5回目で正常リターン
    Storage::shouldReceive('makeDirectory')
      ->once()
      ->andReturnSelf();
    $result = StorageRetrySample::retryMakeDirectory('test');
    $this->assertEquals('success', $result);
  }
}

軽く説明

メソッド説明
shouldReceive(‘メソッド名’)指定したメソッドの返値を指定できる
atMost()少なくとも
once()一度だけ
times(数値)繰り返す回数を指定できる
andReturnSelf()本来のメソッドの返値を使う
andThrow(Exceptionを指定)指定したExceptionを投げる

躓きポイント

自分だけなのかもしれませんが、実際にこのテストを組もうと試行錯誤したポイントとしては
一度モックを定義すると、正常リターンが返ってこなくなるという点です。

shouldReceive()を定義することで、モックが使われるようになります。
ずーっとエラーが発生するケースはこれだけで良いのですが、
5回までリトライするという機能なので、境界値テストとして

  • 4回まで失敗して良い。
  • 5回失敗したらエラー。

という2ケースは必ず必要となると思います。

なので、4回まで失敗させて、5回目で成功させる必要があるのですが、
shouldReceive()->atMost()->times(4)としても、
5回目までエラーが出てしまいました。

そこで、andReturnSelf()の出番です。
これを指定すると、本来のメソッドの返値が復活するので、
エラーが発生しなくなります。

まとめ

如何でしたでしょうか

ネットでshouldReveive関連をググっても
andReturnSelf()を使う例があまり見られず、
単純にエラーを起こしてcatchできるか?
というサンプルが多かった気がします

今回のように、「n回までエラーOKのようなケース」は
多いと思うので、覚えておくと良いと思います。

ではまた!