😽

PHPUnitのテスト分割と並列実行(ParaTest)を試してみた

に公開

この記事について

  • PHPUnitの分割実行の導入・簡単な検証をしました
  • 導入の流れや、検証中にハマったポイントなどについてまとめています

使用マシン/ツール

macOS Ventura ver13.5.2
Docker Desktop ver4.17.0
Laravel Framework 10.21.0
ParaTest v7.2.6

ベースにした環境(docker-laravel)

https://github.com/ucan-lab/docker-laravel
インストールするとPhpUnitを含めた環境が構築できるのでこちらを使用しました。

PHPUnit分割実行のプラグイン(ParaTest)

https://github.com/paratestphp/paratest

導入手順

1. docker-laravelをインストール

ドキュメントどおりにインストールを実行します。
https://github.com/ucan-lab/docker-laravel#laravel-install

http://localhost/ が見れれば大丈夫です。

2. ParaTestをインストール

下記コマンドを実行します

docker compose exec app composer require brianium/paratest --dev

インストールはこれだけです。

コマンド実行

ParaTestの実行は、PHPUnit実行コマンドに --parallel をつけるだけで実行できます。
試しに実行してみます。
(--processes=2 は並列実行するプロセス数を指定するオプションで、デフォルト値はマシンによって変わるようです)

$ docker compose exec app php artisan test --parallel --processes=2

ParaTest v7.2.6 upon PHPUnit 10.3.3 by Sebastian Bergmann and contributors.

Processes:     2
Runtime:       PHP 8.1.23
Configuration: /workspace/phpunit.xml

..                                                                  2 / 2 (100%)

Time: 00:02.317, Memory: 22.00 MB

OK (2 tests, 2 assertions)

実行できました。
追加でテストコードを作成して実行してみます。

/tests/Unit/ExampleTest.php を元に、Parallel1TestParallel2Test を下記内容で作成します。

/tests/Unit/Parallel1Test.php|Parallel2Test.php
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class Parallel1Test extends TestCase
{
    public function test_that_true_is_true1(): void
    {
        $this->assertTrue(true);
    }

    public function test_that_true_is_true2(): void
    {
        $this->assertTrue(true);
    }

    public function test_that_true_is_true3(): void
    {
        $this->assertTrue(true);
    }

    public function test_that_true_is_true4(): void
    {
        $this->assertTrue(true);
    }

    public function test_that_true_is_true5(): void
    {
        $this->assertTrue(true);
    }
}
$ docker compose exec app php artisan test --parallel --processes=2

ParaTest v7.2.6 upon PHPUnit 10.3.3 by Sebastian Bergmann and contributors.

Processes:     2
Runtime:       PHP 8.1.23
Configuration: /workspace/phpunit.xml

............                                                      12 / 12 (100%)

Time: 00:02.096, Memory: 22.00 MB

OK (12 tests, 12 assertions)

追加した分だけ実行できているようです。

ちなみに通常実行した場合は下記のようになりました。

$ docker compose exec app php artisan test
   PASS  Tests\Unit\ExampleTest
 that true is true                                                                                                               0.02s  

   PASS  Tests\Unit\Parallel1Test
 that true is true1                                                                                                              0.01s  
 that true is true2
 that true is true3
 that true is true4
 that true is true5

   PASS  Tests\Unit\Parallel2Test
 that true is true1
 that true is true2
 that true is true3
 that true is true4
 that true is true5

   PASS  Tests\Feature\ExampleTest
 the application returns a successful response                                                                                   0.54s  

  Tests:    12 passed (12 assertions)
  Duration: 0.80s

テストの中身が軽いので、通常の方が早く実行されてしまうようです。

DBを使用した実行検証

次にDBも使用したテストを作ってみます。
今回は既存のUserテーブルにレコードを作成し、テスト側で呼び出すようにしてみます。

テスト実行時にテストユーザーを作成、テスト終了時にテストユーザーを削除するため、/tests/TestCase.php に下記を追記します。

/tests/TestCase.php
abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

+    protected User $testUser;

+    protected function setUp(): void
+    {
+        parent::setUp();
+        /** @var User $testUser */
+        $this->testUser = User::query()->create([
+            "name" => "test",
+            "email" => "",
+            "password" => "test-password",
+        ]);
+    }
    
+    protected function tearDown(): void
+    {
+        User::query()->truncate();
+        parent::tearDown();
+    }
}

次に、Parallel1Test.phpParallel2Test.phpを下記のように変更します。

/tests/Unit/Parallel1Test.php|Parallel2Test.php
- use PHPUnit\Framework\TestCase;
+ use Tests\TestCase;

class Parallel1Test extends TestCase
{
+    use DatabaseTransactions;

    public function test_that_true_is_true1(): void
    {
        // 以降のメソッドも同様に変更
-        $this->assertTrue(true);
+        $this->assertEquals(
+            $this->testUser->name,
+            User::find($this->testUser->id)->name
+        );
    }   

ここで一度実行してみます。

$ docker compose exec app php artisan test --parallel --processes=2

ParaTest v7.2.6 upon PHPUnit 10.3.3 by Sebastian Bergmann and contributors.

Processes:     2
Runtime:       PHP 8.1.23
Configuration: /workspace/phpunit.xml

.......EEEEE                                                      12 / 12 (100%)

Time: 00:02.475, Memory: 22.00 MB

There were 5 errors:

1) Tests\Unit\Parallel1Test::test_that_true_is_true1
Illuminate\Database\QueryException: SQLSTATE[42000]: Syntax error or access violation: 1044 Access denied for user 'phper'@'%' to database 'laravel_test_2' (Connection: mysql, SQL: drop database if exists `laravel_test_2`)

~省略~

Caused by
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1044 Access denied for user 'phper'@'%' to database 'laravel_test_2'

エラーが発生しました。

エラー内容:DBのユーザーに権限がない

https://laravel.com/docs/10.x/testing#parallel-testing-and-databases
ドキュメントによると、ParaTest実行時は内部でテスト用のDBを作成しているようなのですが、 docker-composeで作成しているDBユーザー phepr にその権限を付与していませんでした。

mysql> SHOW GRANTS FOR 'phper'@'%';
+----------------------------------------------------+
| Grants for phper@%                                 |
+----------------------------------------------------+
| GRANT USAGE ON *.* TO `phper`@`%`                  |
| GRANT ALL PRIVILEGES ON `laravel`.* TO `phper`@`%` |
+----------------------------------------------------+

試しに下記クエリでphperに権限を付与して再度テストを実行してみました。

GRANT ALL PRIVILEGES ON *.* TO 'phper'@'%';
FLUSH PRIVILEGES;
$ docker compose exec app php artisan test --parallel --processes=2

ParaTest v7.2.6 upon PHPUnit 10.3.3 by Sebastian Bergmann and contributors.

Processes:     2
Runtime:       PHP 8.1.23
Configuration: /workspace/phpunit.xml

............                                                      12 / 12 (100%)

Time: 00:03.677, Memory: 22.00 MB

OK (12 tests, 12 assertions)

エラーなく実行できました。

テスト実行用のDBユーザーを作成してみる

これでも分割テストが実行できますが、DBユーザーに権限を付与するのではなく、テスト用のDBユーザーを作成し、テスト実行時のみテストユーザーを使うようにしてみようと思います。

今回は/docker-compose.ymlを変更し、環境構築時にテスト用のDBユーザーを作成します。

  • infra/docker/mysql/mysql_init/init.sql を下記内容で作成します
infra/docker/mysql/mysql_init/init.sql
CREATE USER 'tester'@'%' IDENTIFIED BY 'phpunittest';
GRANT ALL PRIVILEGES ON *.* TO 'tester'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
  • /docker-compose.ymlを下記のように変更します
/docker-compose.yml
services:
~省略~
  db:
  ~省略~
    volumes:
+     - ./infra/docker/mysql/mysql_init:/docker-entrypoint-initdb.d
  ~省略~
  • make remakeで環境を再構築します

この状態でテストを実行しても、先ほどと同じエラーが発生すると思います。
ここからテスト実行時のみテスト用のDBユーザーを使うようにします。

  • /src/phpunit.xmlに下記内容を追記します
/phpunit.xml
~省略~
    <php>
    ~省略~
+        <env name="DB_USERNAME" value="tester" force="true"/>
+        <env name="DB_PASSWORD" value="phpunittest" force="true"/>
    </php>
 </phpunit>

テストを実行してみます。

$ docker compose exec app php artisan test --parallel --processes=2

ParaTest v7.2.6 upon PHPUnit 10.3.3 by Sebastian Bergmann and contributors.

Processes:     2
Runtime:       PHP 8.1.23
Configuration: /workspace/phpunit.xml

............                                                      12 / 12 (100%)

Time: 00:03.255, Memory: 22.00 MB

OK (12 tests, 12 assertions)

テスト時のみDBユーザーを切り替えて実行できるようになりました。

感想

まだまだ検証の余地はありますが、ParaTestに合わせてテストコードを作成していけば分割実行は可能そうですね。
業務に導入する機会があれば試してみたいと思います。

参考

https://techblog.roxx.co.jp/entry/2023/05/19/190254

EGSTOCK,Inc.

Discussion