本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回はmyfinderこと久森達郎さんで、テーマは「Perlアプリケーションのテストと高速なCI環境構築術」です。テストに利用するさまざまなモジュールから、CI(Continuous Integration、継続的インテグレーション)環境の構築、そして増えたテストを高速に実行する枠組みの構築までを紹介します。
なお本稿のサンプルコードは、本誌サポートサイトから入手できます。
テストの目的
まずはテストを行う目的を整理します。
コードを壊していないことを確認する
1人で開発するものであれば、どんな状況であれ責任を負うのは自分だけですが、チームや組織が複数にまたがる場合にはテストが重要になります。プロダクトが大きくなるにつれ、自分の開発によって想定外の機能にも影響を与えることが多くなります。逆にほかの開発者が行ったコミットによって自分の開発した部分に影響が出ることもあるので、これらを明確にするためにもテストは必要です。
実行・確認を自動化する
テストコードがないと、開発ごとに毎回手動で機能確認することになり、たいへん非効率です。非効率な状態を放置していると、確認漏れが起こったり、場合によっては十分にテストをせずにリリースしたりといった事態に発展しかねません。
自動化されていれば、誰でも気軽に実行でき、プログラムの改修が容易になるという副次的な効果もあります。
Perlアプリケーションのテスト
次に、Perlアプリケーションのテストについてまとめます。
Test::Moreによる基本的なテスト
Perlでテストを書くにあたって利用する最も一般的なモジュールは、Test::Moreでしょう。Perl 5.6.2からコアモジュールに入っているので、最近のPerl環境であればすぐ使い始めることができます。
Test::Moreはとてもシンプルなライブラリです。次のような単純な消費税率計算をするモジュールを例に説明します。
このモジュールのテストコードは次のようになります。
Test::Moreを使ってテストを書いた場合、テストコードの最後にdone_testing;
を記述するのを忘れないようにしてください。このテストコードでは、
- モジュールのuse
- オブジェクトの生成
- メソッドの実装有無
- メソッドが期待した値を返すか
といった基本的な項目を網羅しています。
Test::Moreにはほかにも、
といったメソッドを備えており、値やデータ構造の比較などもできるので、たいていの用途はカバーできるでしょう。
しかし実際のアプリケーションでは、Web APIなどの外部リソースに依存したコードや、時刻などテスト実行時に変化してしまうものも多くあります。そのようなテストも、ほかのテストモジュールを組み合わせて用いることでカバーできます。
Test::Stubによる外部依存テスト
Test::Stubは、外部サービスのAPIをコールしてその結果を利用するようなコードを書く場合などに役立つモジュールです。実際にはリクエストをせず、期待される結果に対してモジュールのテストを行うことができます。
たとえばTest::Stubを用いて、
といったようにHTTP::Responseオブジェクトを返すだけの内容に差し替えることで、実際にHTTPリクエストを行わず結果オブジェクトを返すテストを書くことができます。
Test::MockTimeによる時刻のテスト
テストコードで時刻を扱うようなケースでは、単純にlocaltime
を用いると、実行タイミングによって取得できる時刻が異なるため、テストが通ったり失敗したりと再現性がない状態になってしまうことがあります。そういったときにはTest::MockTimeを用いることで、localtimeの挙動を書き換え、時間を固定してテストを実行できます。
このように、時間の経過や時間を固定したテストは、Test::MockTimeを活用することで比較的簡単に実装できます。
Plack::TestによるWebアプリケーションのテスト
Webアプリケーションのテストには、実際にプログラムをテストサーバにデプロイして行う方法、モックリクエストを使う方法など、さまざまな手法があります。近年ではPSGIとPlackの普及により、Webアプリケーションのプログラムとテストがより書きやすくなりました。アプリケーションがPlackベースの場合には、Plack::Testを用いると容易にテストを書くことができます。
上記の例はステータスコード200
およびHello
が返されることを期待した処理です。
$app
の部分が実際のWebアプリケーション実装になります。例では$appは単なるcoderefですが、実際には次に示すように、実装したアプリケーションのモジュールをテストする流れになります。
Test::mysqldによるMySQLを利用するテスト
MySQLを利用するテストの場合、Test::mysqldを用いると、テストごとにクリーンなMySQLを起動させることができます[1]。
このテストで起動したMySQLは$mysqld
の参照スコープを抜けた段階で自動的に終了処理が行われ、テストで使われたデータベースがシャットダウンされます。
1つのテーブルのみを相手にする場合は上記のような形でもそれほど問題はありませんが、通常のアプリケーションでは複数のテーブルを利用することもあるでしょう。個別にDDL(Data Definition Language)を書いているとテストのメンテナンスが大変になるため、そういったときはテストの共通処理モジュールを作ってそちらに委譲するか、Test::Fixture::DBIなどの支援モジュールを用いることでテスト用データを容易に取り込むことができます。Test::Fixture::DBIの使い方については、本誌ムック『Perl徹底攻略』の記事[2]を参照してください。
Test::Memcachedによるmemcachedを利用するテスト
Test::mysqld以外にも、ミドルウェアのテストモジュールはたくさん存在します。たとえばmemcachedであればTest::Memcachedを用いることで、テストごとにmemcachedを立ち上げることができます。
memcached のほかにも、RedisであればTest::RedisServer など、さまざまなテストモジュールがCPANにリリースされているので、新しいミドルウェアを検討する際には探してみるとよいでしょう。
proveコマンドによるテストの実行と結果出力
ここまで、各種Perlアプリケーションにおけるテストの基本とその類型を紹介しました。本項では、それらのテストを実行する枠組みと結果出力について取り上げます。
テストが1つだけならば、そのモジュールをuseしてロジックを実行するPerlスクリプト1つあれば事足ります。しかしながら、開発していく中でテストファイルの数が増えていくのは必然です。そういったときにはproveコマンドを用いると、複数のテストをまとめて実行し、TAP(Test Anything Protocol)形式で結果を出力してくれます。
次の例ではtest_dir
以下にあるテストスクリプトを実行し、結果を出力します。
このコマンドは裏でTAP::Harnessというモジュールを利用しています。
TAP::Harnessのしくみ
Perlのテスト実行を担うモジュールであるTAP::Harnessについて説明します。
TAP::HarnessはPerlのテストフレームワークで、proveコマンドで実際にテストを実行するモジュールです。このモジュールが持つruntests
メソッドにテストファイルの名前を渡すと、そのテストを実行します。
このような形でPerlのテストを実行すると、先のprove
コマンドと同様に、標準出力にテスト結果がTAP形式で出力されます。
このTAP::Harnessは後述する高速なCIクラスタの構築における中核モジュールとなるので、覚えておいてください。
<続きの(2)はこちら。>