管理をしてくれるモジュールです。Test::mysqld
を用いてmysqldを起動するテストコードを次に示します。
(1)で、まずTest::mysqld
でmysqldを起動します。そのあとに、(2)で起動したmysqldのDSN(Data Source Name)を用いて$dbh
を作ります。ここで起動したmysqldにはデータベースtest
が存在しますが、その中のテーブル定義は空です。ですので(3)のように、CREATE
文などのDDL(Data Definition Language)を実行します。
DDLを実行したあとに、(4)のように$dbh
をMySQLが使われるモデルオブジェクトに渡すと、テスト中では起動したmysqldが使われます。
Test::mysqld
を使うと、実行される環境にあらかじめmysqldを立てておく手間がなくなります。そして、すでに立っているmysqldの状態に依存しなくなります。常に初期化されたmysqldが使われることで、テストの結果が揺れるなどの、mysqldを使ったテストでありがちな問題が起こりません。また、独自のmysqldを起動することで、テスト向けにチューニングされた設定を常に使えます。
Test::mysqldの注意点
テスト専用のmysqldを起動するときに気を付けるべき点もいくつかあります。
mysqldを起動するためのリソースを余分に消費する
ローカルにある開発用のmysqldをテスト中でも用いる一般的な手法に比べ、専用のmysqldを起動する手法では、mysqldが2つ以上立つことになり、テスト実行マシン上のCPUやメモリ、ディスクなどを余計に消費します。テストを並列実行しないのであれば問題になることはありませんが、並列実行の際に1テストワーカあたり1つのmysqldを割り当てる実装にすると、大きな問題になります。
この問題に対処する最も良い方法は、フルテストでは良いスペックのCI(Continuous Integration、継続的インテグレーション)サーバを用いることです。また、1つのmysqldに1つのデータベースを収容するのではなく、複数のデータベースを受け持って、起動するmysqldの数を抑えることも考えられます。
テストが始まるまでの起動時間が長くなる
Test::mysqld
を使うと、テーブルやレコードが入っていない空のmysqldが起動します。テストに用いるためには、CREATE
文の流し込みや、マスタデータやテストデータのローディングが必要となります。
筆者の担当しているサービスのテーブル数は多く、DDLの実行だけでも多くの時間を使います。また、マスタデータも大量にあるため、マスタデータの入力もテスト実行時のオーバーヘッドとなります。
複数のテストを実行するときにも問題が起こります。テストファイルの中でmysqldを起動していると、mysqldはテストファイルの実行が終わったあとすぐに終了します。そして、ほかのテストファイルを実行する際にもまたmysqldを起動する作業が発生します。このままではテストコード実行よりも、テストを実行するためにmysqldを準備する時間のほうが多くなります。
Test::mysqldの起動高速化のためにデータを流用する
mysqldの起動時間のせいで、テストの実行時間が長くなってしまう問題を解決する方法を考えてみます。
Test::mysqld
によるmysqldの起動には時間がかかります。その大半が、一時ディレクトリにibdata
などのデータ領域を含んだディレクトリを作成することに費やされています。このディレクトリをテスト起動時に毎回作らないようにします。あらかじめ作成しておき、できるだけ再利用して高速化を図ります。
そのために、Test::mysqld
にはcopy_data_from
オプションが存在します。これは、あらかじめ作成したMySQLのデータディレクトリを保存しておき、オプションにディレクトリパスを指定することで、mysqldの起動時にコピーして使用する機能です。
以下に、データディレクトリを作るスクリプトと、それを使う例を示します。
このスクリプトを実行して表示された環境変数を作るコマンドを実行して、テストを実行します。
テストコードでの例は次のようになります。
これで高速にmysqldが起動し、すぐにテストが実行されます。
App::Prove::Plugin::MySQLPoolによるプーリング
mysqldの起動時間を短くする方法を挙げましたが、複数のテストファイルを実行する際に、mysqldプロセスを都度立てずに再利用することを考えてみます。筆者がメンテナーをしているApp::Prove::Plugin::MySQLPool
は、ワーカごとにmysqldを流用する機能を提供するprove
向けのプラグインモジュールです。
使用するには、prove
の起動時に指定します。
テストファイルの中では、環境変数に記述されたDSNを用いてMySQLに接続します。
テストファイルごとにmysqldが起動せず、ワーカごとに再利用できます。
mysqldのプーリングを行う際の注意点
mysqldプロセスを再利用することに関して注意点があります。それは、mysqldに保存されるテーブルやレコードも、前のテストファイルが使用したまま残っていることです。
テーブルは流用できるので、むしろ残っていたほうがテスト実行の高速化に役立つと言えますが、レコードはそうではありません。テストファイルの実行順によっては、前のテストファイルが残したレコードによってfail
することが考えられます。
そのため、テストファイル全体を単一のトランザクション内とし、テスト終了時にロールバックしてレコードの保存を避ける手法がありますが、筆者はお勧めしません。テストを行うアプリケーション内でもトランザクションが実行されており、衝突するからです。
あるいは、入れ子になったトランザクションをPerlプログラム側で実現するCPANモジュールであるDBIx::TransactionManager
を用いれば、トランザクションが衝突せずに解決できたように見えます。しかし、今度はトランザクション外でのSQL実行やトランザクションを分離したテストが実行できなくなります。
プーリングされたmysqldの初期化
そこで提案したいのが、1つのテストが終了したときに、すべてのテーブルを初期化する手法です。
以下は、レコード内のテーブルを列挙するDBIx::Inspector
を使用して、すべてのテーブルを初期化する例です。
トランザクションを実行してロールバックするのに比べて時間はかかりますが、テーブル内のデータを確実に削除して、実行環境をそろえることができます。
Harrietを用いてプーリングを行う
App::Prove::Plugin::MySQLPool
は簡単にmysqldプロセスを再利用できるモジュールですが、デメリットがいくつかあります。copy_data_from
などのオプションには対応していない点、mysqldの起動とテストの実行を切り離せない点などです。
mysqldをプーリングするには、Harriet
を使う方法もあります。Harriet
はApp::Prove::Plugin::MySQLPool
と同様、prove
のプラグインとして動作するモジュールです。ただし、MySQLに特化しているのではなく、テストの前に必要なミドルウェア全般に用いることができます。
mysqld専用であるApp::Prove::Plugin::MySQLPool
と比べて、汎用的なHarriet
では余計にコードを書く必要がありますが、mysqldの使われ方を柔軟にコントロールできます。また、テスト起動時に常に立ち上げて、テストが終わると終了する使い方以外にも、あらかじめmysqldを立てておき、テストではそれを用いる使い方もできます。CIサーバではなく、ローカル開発時にテストを実行する際に、このやり方は効果を発揮します。
Harrietの使用例
使用するには、t/harriet/mysqld.pl
に次のコードを書きます。
そして、テスト実行時にプラグインを指定します。
テストの中では、App::Prove::Plugin::MySQLPool
のときと同様、環境変数TEST_MYSQLD_DSN
にDSNが入ります。これを用いて$dbh
を作ればMySQLにつなぐことができます。
Harrietを使用した場合の注意点と解決策
ただし、これでは並列実行した際に1つのmysqldにつなぐ状態です。この問題を解決するには、
- mysqldを
t/harriet/mysqld.pl
の中で複数個立てておき、ワーカごとに分散させる
- 1つのmysqldの中に複数個のデータベースを用意して、ワーカごとに使う
などの手法が考えられます。
Test::mysqld
には複数個のmysqldを一気に起動するstart_mysqls
メソッドがあるため、mysqldの複数起動は容易です。
ただ、「ワーカごとに分散させる」が難点です。手法としては、次のものが考えられます。
- pidで使うmysqldやデータベースを分ける
- 何らかのデータストアに使用されているmysqldやデータベースを保存しておき、テストファイル起動時に未使用のデータベースを取得する
prove
ではない並列実行を行う独自のテストコマンドを用いて、ジョブワーカ起動時にワーカ番号を環境変数で渡し、それによって使用するmysqldやデータベースを決定する
いずれもメリット/デメリットがありますが、筆者は3番目の手法を、prove
を並列に走らせて結果を統合するコマンドであるgo-prove
を用いて実現しています。
<続きの(3)はこちら。>
- 特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
- 特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
- 特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT