Laravel HTTPテストで419エラーが発生した時の対処法

## 環境

- PHP: 8.1.5
- Laravel: 9.9.0

## テストコード

```php:tests/Feature/Http/Controllers/Auth/LoginControllerTest.php
<?php declare(strict_types=1);

namespace Tests\Feature\Http\Controllers\Auth;

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

final class LoginControllerTest extends TestCase
{
    use RefreshDatabase;

    /**
     * @return void
     */
    public function testSuccess()
    {
        User::factory()->create(['email' => 'test@example.com']);

        $response = $this->postJson('/login', [
            'email' => 'test@example.com',
            'password' => 'password',
        ]);

        $response->assertStatus(200);
        $response->assertJson([
            'data' => true,
            'message' => 'Login successful.',
        ]);
    }
}
```

ログインが成功するかどうかのテストを書いてます。

## テストコードの実行結果

```
$ ./vendor/bin/phpunit

1) Tests\Feature\Http\Controllers\Auth\LoginControllerTest::testSuccess
Expected response status code [200] but received 419.
Failed asserting that 200 is identical to 419.

/data/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:178
/data/tests/Feature/Http/Controllers/Auth/LoginControllerTest.php:26
```

200が返ってきて欲しいところが419HTTPステータスコードが返ってきてしまっています。

## 念の為、キャッシュクリアする

https://readouble.com/laravel/9.x/ja/testing.html#environment

テストを実行する前は必ず `config:clear` Artisanコマンドを使用して設定のキャッシュをクリアしてください。

```shell-session
$ php artisan config:clear
```

`php artisan config:cache` していなければ `bootstrap/cache/config.php` のキャッシュファイルも作られないので不要です。
念の為、実行しましましたが変化はありませんでした。

## 419 TokenMismatchException

419が返る時はTokenMismatchExceptionが発生しています。
フレームワークのコードでいうとこの部分です。

https://github.com/laravel/framework/blob/9.x/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php#L85

```php:src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php
    public function handle($request, Closure $next)
    {
        if (
            $this->isReading($request) ||
            $this->runningUnitTests() ||
            $this->inExceptArray($request) ||
            $this->tokensMatch($request)
        ) {
            return tap($next($request), function ($response) use ($request) {
                if ($this->shouldAddXsrfTokenCookie()) {
                    $this->addCookieToResponse($request, $response);
                }
            });
        }

        throw new TokenMismatchException('CSRF token mismatch.');
    }
```

VerifyCsrfTokenミドルウェアでトークンが見つからなかったら例外を返しています。
テストコードに毎回Csrfトークンを仕込んだり、ミドルウェアを除外して対応するのは論外というか負けた気がしますね。

ここのコードを見て思うのは、 `$this->runningUnitTests()` あるからテスト時は例外発生しないのでは？

## configの環境を確認するテスト

下記のコードを追加してテスト試します。

```
$this->assertEquals('testing', config('app.env'));
```

```
$ ./vendor/bin/phpunit

1) Tests\Feature\Http\Controllers\Auth\LoginControllerTest::testSuccess
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'testing'
+'local'
```

`testing` になって欲しいところが、 `local` になってます。

## phpunit.xml

`phpunit.xml` を確認します。

```xml:phpunit.xml
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
        <!-- <env name="DB_DATABASE" value=":memory:"/> -->
        <env name="MAIL_MAILER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
    </php>
```

`APP_ENV` に `testing` が指定されているので良さそうに思います。

## Laravel 8.x と 9.x

https://github.com/laravel/laravel/blob/8.x/phpunit.xml
https://github.com/laravel/laravel/blob/9.x/phpunit.xml

`server` タグから `env` タグに変更されています。
変更されたプルリクは下記です。

https://github.com/laravel/laravel/pull/5765

## 自分の過去記事より

https://qiita.com/ucan-lab/items/42c1814d8bd69895374c#%E8%A3%9C%E8%B6%B3-%E9%96%8B%E7%99%BA%E7%94%A8%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%81%8C%E6%B6%88%E3%81%88%E3%82%8B%E5%A0%B4%E5%90%882

Laravel7系時点の記事ですが、`server` タグから `env` タグに変更して `force` 属性を付与していました。

```
$ echo $APP_ENV
local
```

サーバー環境変数の `APP_ENV` に `local` が指定されているのでこちらが優先されているようです。

```xml:phpunit.xml
    <php>
        <env name="APP_ENV" value="testing" force="true"/>
    </php>
```

これで動くのでは？

```
$ ./vendor/bin/phpunit

1) Tests\Feature\Http\Controllers\Auth\LoginControllerTest::testSuccess
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'testing'
+'local'
```

動かない。

## env から server タグに変更する

Laravel8系の頃のserverタグに戻して試します。

```xml:phpunit.xml
    <php>
        <server name="APP_ENV" value="testing"/>
    </php>
```

```
$ ./vendor/bin/phpunit 
PHPUnit 9.5.20 #StandWithUkraine

....                                                                4 / 4 (100%)

Time: 00:01.638, Memory: 30.00 MB

OK (4 tests, 6 assertions)
```

動いた...なぜ？

https://phpunit.readthedocs.io/ja/latest/configuration.html?highlight=server#php-ini
phpunitのドキュメントを見てもよくわからない...

## 結論

サーバー環境変数の `APP_ENV` に `testing` 以外の値が設定されている場合。

テスト実行時は `APP_ENV` が `testing` の時はCSRFトークンの例外は発生しないので、
`phpunit.xml` の `env` 要素を `server` 要素に変更すると動く。

## さいごに

どうしてこんなことになったのか、私にはわかりません。
これを読んだあなた。
どうか真相を暴いてください。
それだけが私の望みです。