2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-03-30

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

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

実行順序の確認
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];
    }
}
実行結果
.\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を使わず独立したテストケースにする
  2. クロージャを使用して$thisをバインドする
  3. クロージャを使用して$thisを引数として渡す

DataProviderを使わず独立したテストケースにする

無理に共通化することでテストコードの可読性が下がる可能性もあるため最初に検討するべき方法です。

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 を解析できず警告が出るのでおすすめできません。

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での表示
VSCodeの警告。

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

クロージャ実行時の引数として $this を渡すことで型ヒントや静的解析の支援も受けることができます。

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クラスでユーザー登録時のバリデーション条件テスト。

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'
        ];
        // その他のテストケース
    }
}
2
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?