【Laravel】例外・バリデーション・メール送信のテストコードをPHPUnitで書く

## はじめに

久しぶりにPHPの案件をやることになり、
テストコードについて調べる機会が増えたので、よく使うものを列挙します！

## コードサンプル
```php:sample.php
/**
* {@inheritdoc}
*/
public function setup(): void
{
    parent::setup();

    $this->user = User::factory()->create(
        [
            'email' => 'test@gmail.com',
            'first_name' => 'テスト',
            'last_name' => '太郎',
        ]
    );

    $this->app->bind(CognitoServiceInterface::class, fn ($_) => new MockCognitoService());
}

/**
 * @test
 */
public function メールアドレス更新処理で例外が発生する()
{
    $response = $this->get(route('sample.index'));
    $response->assertRedirect(route('auth.login'));

    $this->actingAs($this->user);

    $this->instance(
        SampleService::class,
        Mockery::mock(SampleService::class, function ($mock) {
            $mock->shouldReceive('updateEmail')->once()->andThrow(RuntimeException::class);
        }),
    );

    $response = $this->put(route('sample.update_email', ['email' => self::UPDATE_EMAIL]));

    $response->assertRedirect(route('sample.index'));
    $response->assertSessionHas(
        'error',
        fn ($message) => $message == __('messages.error.unexpected'),
    );
}

/**
 * @test
 */
public function メールアドレス更新処理がバリデーションエラーになる()
{
    $response = $this->get(route('sample.index'));
    $response->assertRedirect(route('auth.login'));

    $this->actingAs($this->user);

    $params = $this->getUpdateParams();
    Arr::set($params, 'email', '');

    $response = $this
        ->from(route('sample.edit'))
        ->put(route('sample.update', $params));

    $this->assertDatabaseMissing('users', $params);

    $response->assertInvalid(['email' => 'メールアドレスは必須です。']);
    $response->assertRedirect(route('sample.edit'));
}

/**
 * @test
 */
public function メールアドレス変更確認メール送信に失敗する()
{
    Mail::fake();

    $this->mock(SampleService::class, function (MockInterface $mock) {
        $mock->shouldReceive('sendEmail')->once()->andThrow(InternalErrorException::class);
    });

    /** @var SampleService $service */
    $service = app(SampleService::class);

    $this->assertThrows(
        fn () => $service->sendEmail(
            self::UPDATE_EMAIL,
            $this->user
        ),
        InternalErrorException::class,
    );

    Mail::assertNotSent(UpdateMail::class);
}
```


## 前処理
setup() で処理をするようにしています。

ここではテストコードに必要な処理を記載しています。


## 共通
```php:common.php
    $response = $this->get(route('sample.index'));
    $response->assertRedirect(route('auth.login'));

    $this->actingAs($this->user);
```

assertRedirect はログイン画面にリダイレクトし、
actingAs でテストケース内で、ログイン状態が維持するようにしています。

## 例外（Exception）のテスト
```php:exception.php
$this->instance(
        SampleService::class,
        Mockery::mock(SampleService::class, function ($mock) {
            $mock->shouldReceive('updateEmail')->once()->andThrow(RuntimeException::class);
        }),
    );
```
ここでは、Mockery を使って例外を発生させています。
具体的には、SampleServiceで定義されている「updateEmail」でRuntimeExceptionを発生させています。


```php
try {
    // 
    
} catch (RuntimeException $e) {
    throw new Exception('エラー');
} catch (Exception $e) {
    throw $e;
}
```
Controller や Service などで上記のような try catch をする箇所がおそらく発生すると思うのですが、ここを想定したテストになります。

## バリデーションのテスト
```php:validation.php
    $this->assertDatabaseMissing('users', $params);

    $response->assertInvalid(['email' => 'メールアドレスは必須です。']);
    $response->assertRedirect(route('sample.edit'));
```

- assertDatabaseMissing 
    - usersテーブルに $paramsのデータが登録されていないかチェックします。

- assertInvalid
    - 想定されるバリデーションエラーメッセージをチェックします。
- assertRedirect
    - バリデーションエラー発生時のリダイレクト先をチェックします。

## メール送信のテスト
```php:email.php
    Mail::fake();

    ...

    Mail::assertNotSent(UpdateMail::class);

```

- Mail::fake()
    - 実際にはメールを送らないように設定します。
- Mail::assertNotSent()
    - メール送信されたことをチェックします。

## まとめ
例外・バリデーション・メール送信をピックアップしてテストコードの一部をまとめました。

実務で今まであまりテストコードを書いてなかったので、よく使うものがあればまた紹介したいと思います。