CakePHP 4.x に ParaTest を導入
テストの数が増えてきて全件テストを実行するのに時間がかかるようになった為、ParaTestを導入し、テストを並列実行するようにしてみました。
version
ドキュメント
ParaTest
- PHPUnit でのテストを並列で実行できるツール
- 並列数(プロセス数)はオプションで指定できる
- テストクラス単位で並行実行される模様
- 環境変数
ParaTestに並列実行されるプロセスごとに1から始まる連番が設定される- DB を利用したテストを並列実行する際は、
TEST_TOKENを利用して、各プロセス毎に利用するDBを分けるが必要がある
- DB を利用したテストを並列実行する際は、
- Laravel 10.x の並列テストでは内部的に ParaTest を使用
導入効果
元々 06:27 かかっていたところが 01:48 で実行できるようになりました
ParaTest (8 プロセス):01:48PHPUnit (1 プロセス):06:27
CakePHP 4.x への導入方法のメモ
DB が絡むテストを並列実行する際は、
TEST_TOKENを利用して、各プロセス毎に利用するDBを分ける等の工夫が必要になる
- 今回はCakePHPのコアのコードを読み、以下のようにする事で、各プロセス毎に利用するDBの出し分けを実現しました
- Fixtureの調整あたりで少しハマりました
ParaTest をインストール
composer require --dev brianium/paratest --with-all-dependencies
並列数分のテスト用DBを作成する
- 今回は 8 プロセスで並列実行する想定なので、並列数分のテスト用DBを作成
- 作成方法は割愛
app_local.php の Datasources に ParaTest 用の connection の設定を追加
- CakePHPの内部的な実装の都合で connection 名を
test_で始まるものにする必要がある
// app/config/app_local.php
$paraTestConnectionBase = [
'className' => Connection::class,
'driver' => //...,
//...
'host' => env('DB_HOST'),
'port' => env('DB_PORT'),
'username' => env('DB_USER'),
'password' => env('DB_PASSWORD'),
'encoding' => env('DB_ENCODING', 'UTF8'),
'timezone' => env('DB_TIMEZONE', 'UTC'),
]
// ...
return [
//...
'Datasources' => [
// ...
// ParaTest用のconnection
// note: connection名を `test_` で始まるものにする必要がある
'test_1' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test1'],
'test_2' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test2'],
'test_3' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test3'],
'test_4' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test4'],
'test_5' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test5'],
'test_6' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test6'],
'test_7' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test7'],
'test_8' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test8'],
TEST_TOKEN を元に、各プロセス毎の Connection の設定を調整
\Cake\TestSuite\Fixture\PHPUnitExtension::executeBeforeFirstTestの処理をカスタマイズする必要があった為、PHPUnitのBeforeFirstTestHookを利用し、以下のようにカスタマイズ
// app/phpunit.xml.dist
//...
<extensions>
<extension class="Path\To\AppExtension"/>
//...
// Path/To/AppExtension.php
//...
use PHPUnit\Runner\BeforeFirstTestHook;
//...
class AppExtension implements BeforeFirstTestHook
{
/**
* what:
* \Cake\TestSuite\Fixture\PHPUnitExtension::executeBeforeFirstTest の処理に、
* ParaTest 使用時の connection を調整する為の処理を追加したもの
*/
public function executeBeforeFirstTest(): void
{
$helper = new ConnectionHelper();
$helper->addTestAliases();
// for ParaTest
$paraTestToken = getenv('TEST_TOKEN');
if ($paraTestToken !== false) {
// ParaTestで各プロセス用のconnectionを設定する為に、
// \Cake\TestSuite\ConnectionHelper::addTestAliases で設定した ConnectionManager::alias('test', 'default'); を上書き
ConnectionManager::alias("test_{$paraTestToken}", 'default');
}
$enableLogging = in_array('--debug', $_SERVER['argv'] ?? [], true);
if ($enableLogging) {
$helper->enableQueryLogging();
Log::drop('queries');
Log::setConfig('queries', [
'className' => 'Console',
'stream' => 'php://stderr',
'scopes' => ['queriesLog'],
]);
}
}
Fixture で利用する connection を調整
TEST_TOKENを元に、当該プロセス用のconnectionを設定
// app/tests/Fixture/AppTestFixture.php
//...
use Cake\TestSuite\Fixture\TestFixture;
//...
class AppTestFixture extends TestFixture
{
public function __construct()
{
// ParaTestの場合は、当該プロセス用のconnectionを設定
$paraTestToken = getenv('TEST_TOKEN');
if ($paraTestToken !== false) {
$this->connection = "test_{$paraTestToken}";
}
parent::__construct();
}
}
class XxxxFixture extends AppTestFixture
Test用DBのmigration方法を調整
- こちらも
TEST_TOKENを元に当該プロセスで利用するDBをmigrateするように調整 - 各DB毎に1回だけmigrateされるように調整
// app/tests/bootstrap.php
// Run migrations for multiple plugins
$migrator = new Migrator();
$paraTestToken = getenv('TEST_TOKEN');
// for ParaTest
if ($paraTestToken !== false) {
$dirName = TMP . 'tests' . DS . 'paratest' . DS;
$fileName = "migrated-test_{$paraTestToken}";
$path = $dirName . $fileName;
// 1DBにつき1回のみmigrate
if (!file_exists($path)) {
$DBName = "test_{$paraTestToken}";
$connectionName = $DBName;
$migrator->runMany([
//...
['connection' => $connectionName, 'plugin' => '//...'],
]);
if (!file_exists($dirName)) {
mkdir($dirName);
}
touch($path);
}
} else {
$migrator->runMany([
//...
['connection' => 'test', 'plugin' => '//...'],
]);
}
上記で作成した一時ファイルをParaTest実行前に削除するように設定
// composer.json
{
//..
"scripts": {
//..
"paratest": "rm -rf tmp/tests/paratest && ./vendor/bin/paratest -p 8",
ParaTest実行
composer paratest