😽

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

2023/10/02に公開

この記事について

  • 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