ParaTest で TEST_TOKEN を使って DB が絡むテストを並列実行する

だいぶ以前に Qiita に ParaTest で PHPUnit を並列実行する記事を書いていたのですが、

よく考えたら別に Docker なんて必要なくて、ひとつの MySQL インスタンスに複数のデータベースを作ればいいだけでした。なんとなく Docker を使ってみたかっただけじゃないかな、この時期。

また ParaTest の TEST_TOKEN を使えばそもそも変なハックしなくても普通に使用するデータベース切り替えられます。ParaTest の README にそのまんま書かれています。

https://github.com/paratestphp/paratest#test-token

<?php
if (getenv('TEST_TOKEN') !== false) {  // Using paratest
    $dbname = 'testdb_' . getenv('TEST_TOKEN');
} else {
    $dbname = 'testdb';
}

TEST_TOKEN が追加されたのが下記のリリースなので、記事を書いた当初はまだ TEST_TOKEN は実装されてなかったようです。

さっそく試してみました。使用したコード類は https://github.com/ngyuki-sandbox/php-paratest-with-db にあります。

普通にテストを実行すると10秒ぐらいかかります。

$ vendor/bin/phpunit
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.

..........                                                        10 / 10 (100%)

Time: 00:10.232, Memory: 6.00 MB

OK (10 tests, 10 assertions)

普通に ParaTest を実行するとテストケースごとにデータベースのフィクスチャをざっくざく入れてるとめちゃめちゃ競合します。

$ vendor/bin/paratest -p 5
Running phpunit in 5 processes with /work/vendor/phpunit/phpunit/phpunit

Configuration read from /work/phpunit.xml.dist

EE.EE.EEEE                                                        10 / 10 (100%)

Time: 00:02.230, Memory: 6.00 MB

There were 8 errors:

1) Test\Sample0Test::test
PDOException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1' for key 't.PRIMARY'

...snip...

FAILURES!
Tests: 10, Assertions: 2, Errors: 8.

接続するデータベース名について、環境変数 TEST_TOKEN をデータベース名のサフィックスに追加します。

<?php
$host     = getenv('MYSQL_HOST');
$port     = getenv('MYSQL_PORT');
$username = getenv('MYSQL_USER');
$password = getenv('MYSQL_PASSWORD');
$dbname   = getenv('MYSQL_DATABASE');
$charset  = 'utf8mb4';

$token = getenv('TEST_TOKEN');
if ($token !== false) {
    $dbname .= $token;
}

TEST_TOKEN には ParaTest での実行時に並列実行されるプロセスごとに1から始まる連番が設定されます。

並列数の分だけデータベースを作成します。マイグレーション適用済のデータベースからダンプ→リストアが手っ取り早です。

# マイグレーションを実行 ... 使用するツールに応じて適切なコマンドに置き換え
cat database/*.sql | mysql -v "$MYSQL_DATABASE"

# データベースをダンプ
mysqldump -h "$MYSQL_HOST" "$MYSQL_DATABASE" -r dump.sql

# 連番サフィックスのデータベースへの権限を付与
mysql -v -e "grant all on \`${MYSQL_DATABASE}%\`.* to $MYSQL_USER@'%'"

# 連番サフィックスのデータベースを作成
seq 5 | xargs -P0 -i mysql -v -e 'create database if not exists test{}'

# 連番サフィックスのデータベースへダンプを流し込み
seq 5 | xargs -P0 -i mysql -e 'source dump.sql' 'test{}'

# テストを実行
vendor/bin/paratest -p 5

10秒かかっていたテストが2秒で終わるようになりました。

Running phpunit in 5 processes with /work/vendor/phpunit/phpunit/phpunit

Configuration read from /work/phpunit.xml.dist

..........                                                        10 / 10 (100%)

Time: 00:02.216, Memory: 6.00 MB

OK (10 tests, 10 assertions)

さいごに

かなり極端な例なので、実際の効果の程は実行環境や並列数によりけりです。手元の実案件で試してみたところ、DBを使うテストが4並列で半分ぐらいの時間で終わるようになりました。