PHPUnit10以降のstaticなdataProviderでもステートフルなケースに対応する

PHPunit10以降ではDataProvider関数はstaticが推奨になりました。
https://docs.phpunit.de/en/10.5/writing-tests-for-phpunit.html#data-providers
staticなので `$this` を使ったテストクラスの状態を参照できなくなります。

そもそもDataProviderはテストケース実行前に呼ばれるため、setUpで作成したデータを参照できません。

```php:実行順序の確認
class ExampleTest extends TestCase
{
    private int $expectValue;

    protected function setUp(): void
    {
        parent::setUp();
        echo "setUp called\n";

        $this->expectValue = random_int(0, 9);
    }

    #[DataProvider('data')]
    public function test(int $actual): void
    {
        echo "testcase called\n";
        $this->assertSame($this->expectValue, $actual);
    }

    public static function data()
    {
        echo "data called\n";
        yield '1' => [1];
    }
}
```

```txt:実行結果
.\vendor\bin\phpunit --filter=ExampleTest
data called
PHPUnit 10.5.15 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.2.4
Configuration: D:\dev\simutrans-portal\phpunit.xml

setUp called
testcase called
F                                                                   1 / 1 (100%)
Time: 00:00.108, Memory: 34.00 MB

There was 1 failure:

1) Tests\Unit\ExampleTest::test with data set #0 (1)
Failed asserting that 1 is identical to 5.
```

## 回避策
既存データ更新時のバリデーションなど状態に依存するケースを扱おうとする場合、以下のような回避策が可能です。
1. DataProviderを使わず独立したテストケースにする
1. クロージャを使用して$thisをバインドする
1. クロージャを使用して$thisを引数として渡す

### DataProviderを使わず独立したテストケースにする
無理に共通化することでテストコードの可読性が下がる可能性もあるため最初に検討するべき方法です。

```php
class ExampleTest extends TestCase
{
    private int $expectValue;

    protected function setUp(): void
    {
        parent::setUp();

        $this->expectValue = random_int(0, 9);
    }

    public function test(): void
    {
        // 実際はアサーション対象メソッドを通しての結果
        $actual = $this->expectValue;
        $this->assertSame($this->expectValue, $actual);
    }

    // その他DataProviderを使ったテストケース
}
```

### クロージャを使用して$thisをバインドする
`Closure::bind` を使用することでクロージャにテストクラスをバインドすることでアクセスできるようになります。
ただしIDEや静的解析では `$this` を解析できず警告が出るのでおすすめできません。

```php
class ExampleTest extends TestCase
{
    private int $expectValue;

    protected function setUp(): void
    {
        parent::setUp();

        $this->expectValue = random_int(0, 9);
    }

    #[DataProvider('data')]
    public function test(Closure $fn): void
    {
        $fn = Closure::bind($fn, $this);
        $actual = $fn();
        $this->assertSame($this->expectValue, $actual);
    }

    public static function data()
    {
        yield '1' => [fn () => $this->expectValue];
    }
}
```

![VSCodeでの表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107279/49688a35-d9fb-3c56-83ba-bb8ab7578b24.png)
VSCodeの警告。

### クロージャを使用して$thisを引数として渡す

クロージャ実行時の引数として `$this` を渡すことで型ヒントや静的解析の支援も受けることができます。
```php
class ExampleTest extends TestCase
{
    private int $expectValue;

    protected function setUp(): void
    {
        parent::setUp();

        $this->expectValue = random_int(0, 9);
    }

    #[DataProvider('data')]
    public function test(Closure $fn): void
    {
        $actual = $fn($this);
        $this->assertSame($this->expectValue, $actual);
    }

    public static function data()
    {
        yield '1' => [fn (self $self) => $self->expectValue];
    }
}
```

## 使用例
LaravelのFormRequestクラスでユーザー登録時のバリデーション条件テスト。

```php
class StoreUserRequestTest extends TestCase
{
    private User $user;

    protected function setUp(): void
    {
        parent::setUp();
        $this->user = User::factory()->create();
    }

    #[DataProvider('dataValidation')]
    public function test(Closure $setup, string $errorKey): void
    {
        $data = $setup($this);
        $request = new StoreUserRequest($data);
        $messageBag = Validator::make(
            $data, 
            $request->rules()  // ['email' => 'unique:users,email']
        ); 
        $this->assertArrayHasKey($errorKey, $messageBag->toArray());
    }

    public static function dataValidation(): \Generator
    {
        yield '登録済みのメールアドレス' => [
            fn (self $self): array => ['email' => $self->user->email],
            'email'
        ];
        // その他のテストケース
    }
}
```