テストだってテストが必要です

c6r
2011-12-09

@ikasam_aさんにTest::Classの話でもとお誘いを受けて安請け合いしたのはいいものの、まじめな話はモダンPerlの連載にそこそこまとまっているので、今日はどうしてTest::Classのようなモジュールを使うのかという話をしてみます。

テストをモジュールの中に移す

Test::Classやその仲間たちは、うまく使えばとても便利なのですが、テストの数が少ないうちは手間ばかりかかるので、なかなか使う機会に恵まれないかもしれません。テストをモジュールの中に移すといわれてもピンとこない方のために、まずは簡単な例をあげておきましょう。

今年のカレンダーでも何度か出てきているように、Perlのテストはふつう.tファイルにべた書きします。

#!perl
use strict;
use warnings;
use Test::More;

pass "simplest test";

done_testing;

テストをモジュールの中に移す、というのは、これを起動用の.tファイルと、

#!perl
use strict;
use warnings;
use Test::Class;
use MyTest;

MyTest->runtests;

実際のテストをおさめるモジュールに分けるということです(ここではTest::Classを継承したりアトリビュートがついたりしていますが、これらは本質とは無関係です)。

package MyTest;
use strict;
use warnings;
use base 'Test::Class';
use Test::More;

sub first_test :Test {
  my $self = shift;

  pass "simplest test";
}

1;

このように、数が少なく内容もシンプルなテストの場合は手間が増えるデメリットばかり目立ってしまうのですが、このような書き方をしておくと、

  • テストにかかる合計時間が減る(かもしれない)
  • テストの一部を使い回しやすくなる(かもしれない)
  • ほかの便利なツールの恩恵を受けやすくなる

といったメリットがあります。以下、具体的に見ていきましょう。

テストにかかる合計時間が減る

たとえばデータベースを使うテストが大量にある場合、まさか本番用のデータベースをそのまま使うわけにもいきませんから、テスト用のデータベースを用意するわけですが、このとき、それぞれの.tファイルでデータベースの設定を行うと、テストファイルが増えれば増えるほどデータベースの初期化や後始末に時間をとられるようになります。とりわけTest::mysqldのように準備に時間がかかるツールを使う場合、テストファイルが変わるたびにMySQLの作成と廃棄を行っていては時間がかかってしかたありません。

Test::Classのようにテストをモジュール形式にしておくと、テストに使うモジュールの数は増えても、それをロードする.tファイルの数はさほど増やさずにすみます。そのため、データベースの作成などは起動用の.tファイルや、Test::Classのstartupフェーズに追い出してしまい、テストモジュールの中ではテーブルの作成や削除だけ繰り返すようにすると、待ち時間を短縮できるようになります。

#!perl
use strict;
use warnings;
use Test::Class::Load 't/lib'; # t/lib以下のテストを全部読み込む
use MyTestDB;

# ここはモジュール中にテストがいくつあっても
# 一度しか実行されません
MyTestDB->startup;

Test::Class->runtests;

MyTestDB->shutdown;

同じ理由で、Mooseなど立ち上がりの遅いモジュールを利用したアプリケーションも、重いモジュールを毎回ロードしなくてよくなる分だけテストの高速化が期待できます。

テストの一部を使い回しやすくなる

テストがモジュールにまとまっているということは、ファイルを読み込んだだけではテストが実行されない、ということでもあります。もちろんテストをモジュールにしてしまわない場合でもt/lib/Utils.pmのようなテスト専用のヘルパーモジュールを用意して共通の処理をまとめることはふつうに行われていることとおもいますが、テストをモジュールの中にまとめてしまうことで、テストの本体は親クラスに押し込み、子クラスではテストに利用する値のみ変える、ということが簡単にできるようになります。ここでは時間の都合でごく簡単な例のみ載せていますが、実際には与えるデータと受け取るデータをそれぞれ配列なりハッシュなりに渡しておいて親クラスで定義したテストの中でループを回し、得られた値を順に比較していくような使い方をするのがよいでしょう。

use strict;
use warnings;
package MyTestBase;
use base qw/Class::Data::Inheritable Test::Class/;
use Test::More;

__PACKAGE__->mk_classdata('fixture');

sub is_even: Test {
  my $self = shift;
  my $fix = $self->fixture;
  if (!defined $fix) {
    SKIP: { skip "fixture is not defined", 1; fail; }
    return;
  }
  my $mod = $fix % 2;
  ok !$mod, "$fix is even";
}

package MyTestA;
use base 'MyTestBase';

__PACKAGE__->fixture(4);

package MyTestB;
use base 'MyTestBase';

__PACKAGE__->fixture(2);

ほかの便利なテストツールの恩恵を受けやすくなる

個人的にはこれがTest::Classのようなツールを使う一番の理由なのですが、テスト用のモジュールは、ほかの便利なツールでテストできます。

たとえば、大量に用意したテストを実行する前に、つまらない構文エラーがないか確認したいとしましょう。もちろん.tファイルの場合でもperlの-cオプションを使えば構文チェックくらいできるわけですが、Test::UseAllModulesなどを使えば、テスト用のモジュールもひっくるめてロードに失敗するモジュールがないか確認できます。

use strict;
use warnings;
use Test::UseAllModules qw(lib t/lib);

all_uses_ok();

あるいは、納品用にテストのドキュメントも必要だということになるかもしれません。Test::Pod::Coverageを使えば、どこでどんなテストをしたのか、説明を書くための圧力になってくれることでしょう。

use strict;
use warnings;
use lib "t/lib";
use Test::More;
use Test::Pod::Coverage;

pod_coverage_ok($_) for all_modules("t/lib");

done_testing;

仕様はわかっているのでテスト項目は列挙できるけれど、実際のテストはまだ書けないような場合は、テストモジュールのPODにテスト名だけ列挙してもらい、あとでそのテストが実装されたか確認する、なんてこともできるはずです。

テストだってテストが必要なんです

何千行もあるモノリシックなCGIスクリプトがダメなら、何千行もあるテストスクリプトだって同じくらいダメなはずですし、テストがないアプリがダメなら、あるいはオブジェクト指向になっていないアプリがダメなら、そのようなテストだってダメなはずです。

ほんとにこのテストがあっているのかわからない、なんてことになってきたら、ぜひTest::Classのようなツールを使ってテストをモジュールに追い出してみてください。ごちゃごちゃしていたテストがすっきりするかもしれませんよ。

明日はzentoooさんです。お楽しみに。