モダンPerlの世界へようこそ

第30回Test::Class:ユニットテストに使うだけでなく

メタデータからテスト件数を取得する

前回はテストファイルやテストデータの数からテストプランを計算するモジュールを紹介しました。今回はその続きとして、テストファイルのメタデータからテストの数を求めるモジュールを紹介していきましょう。これらのモジュールの多くは1994年にケント・ベック(Kent Beck)氏がSmalltalk向けに書いたSUnitを祖先にもつ、いわゆるxUnit系のフレームワークに属するものですが、Perlにはそれ以前からTest Anything Protocolを使った独自のテスト手法が存在していたため、Javaなどで使われている同種のフレームワークとはやや毛色の違う部分もあります。一般的にはクラスをひとつ書くたびに対応するユニットテスト用のクラスを書くのがよいように言われていますが、ここではもっとゆるく、テストを自動的に検出してくれるだけでなく、テストの事前事後になんらかの処理を行うときにも便利なツールという側面に注目してみることにします。

Test::Class

この手のテストモジュールとしては2000年2月にリリースされたTest::Unitが最初期のものになりますが、これは今回紹介するほかのツールに比べるとよりxUnitのやり方に忠実な分、Test::Moreを始めとするほかのテストツール群との相性は悪いため、名前のみの紹介にとどめます。

xUnit的な手法を活かしつつ、Test::Builderを利用する既存のテストツールとの親和性を高めたものとしては、エイドリアン・ハワード(Adrian Howard)氏が2002年にリリースしたTest::Classがあります。そのもっとも基本的な使い方は、このような感じになります。

use strict;
use warnings;
use Test::Class;

Test::Class->runtests;

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

sub startup : Test(startup) {
    my $self = shift;
    $self->{dbh} = DBI->connect('dbi:SQLite::memory:');
    note "startup";
}

sub setup : Test(setup) {
    my $self = shift;
    $self->{dbh}->do('create table foo (id integer primary key, text)');
    note "setup";
}

sub insert : Test {
    my $self = shift;
    ok $self->{dbh}->do('insert into foo values(?, ?)', undef, 1, 'my text');
}

sub teardown : Test(teardown) {
    my $self = shift;
    $self->{dbh}->do('drop table foo');
    note "teardown";
}

sub shutdown : Test(shutdown) {
    my $self = shift;
    $self->{dbh}->disconnect;
    note "shutdown";
}

細かな使い方についてはTest::ClassのPODをご覧いただくとして、ここではTest::ClassがMyTestパッケージ内にあるTest属性のついた5つのメソッドをかき集め、その内容を調べてテストプランを宣言し、まずはstartupのついたメソッドを、続いてsetupメソッド、特殊な指定のないふつうのテストメソッド(insertメソッド⁠⁠、teardownメソッド、最後に後片づけ用のshutdownメソッドを実行する、という流れだけ把握しておいてください。startup/shutdownメソッドとsetup/teardownメソッドの違いは、個々のテストのたびに実行されるかどうかです。

テストを追加してみる

動作を確認するためにもうひとつ、selectというテストを追加してみましょう。ここではデータの挿入をほかのテストで流用しているため、selectテストのテストプランをその分増やしています。

sub select : Tests(2) {
    my $self = shift;
    $self->insert;
    my ($text) = $self->{dbh}->selectrow_array('select text from foo where id = ?', undef, 1);
    is $text => 'my text';
}

筆者の環境ではproveコマンドを使うといささか出力が崩れてしまいましたが、perlを使って直接テストを実行してみると、このようにsetupとteardownはそれぞれのテストの前後に実行される様子が確認できます。

> perl test.t
# startup
# setup
1..3
ok 1 - insert
# teardown
# setup
ok 2 - select
ok 3 - select
# teardown
# shutdown

テストを指定した順番で実行させてみる

このようにsetupとteardownをうまく利用して個々のテストを毎回同じ(独立した)環境でテストできるようにするのがTest::Classの基本ですが、Test::Classには個々のテストをアルファベット順に実行するという特徴があるので、それをうまく利用すると、このようにstartupからteardownまで、シナリオに沿ってテストをしていくこともできます。

sub startup : Test(startup) {
    my $self = shift;
    $self->{dbh} = DBI->connect('dbi:SQLite::memory:');
    $self->{dbh}->do('create table foo (id integer primary key, text)');
    note "startup";
}

sub test01 : Test {
    my $self = shift;
    ok $self->{dbh}->do('insert into foo values(?, ?)', undef, 1, 'my text');
}

sub test02 : Test {
    my $self = shift;
    my ($text) = $self->{dbh}->selectrow_array('select text from foo where id = ?', undef, 1);
    is $text => 'my text';
}

sub shutdown : Test(shutdown) {
    my $self = shift;
    $self->{dbh}->disconnect;
    note "shutdown";
}

なお、今回は単一のテストファイル内にテストを実行する部分(Test::Class->runtests)とテストを宣言する部分をまとめましたが、これらは別々のファイルに格納することもできますし、Test::Classのテストを書くときはむしろそうするほうが普通です。そうする場合はテストの実行部に実行したいテストパッケージをuseする文を追加してください。複数のテストパッケージをuseすればテストをまとめて実行できます(特定のテストパッケージだけを実行したい場合は、MyTest->runtestsのようにパッケージを指定することもできます⁠⁠。

use strict;
use warnings;
use MyTest;

Test::Class->runtests;

Test::Classの仲間たち

Test::Classはダミアン・コンウェイ氏が2001年にリリースしたAttribute::Handlersというモジュールを利用してテストメソッドを集めていますが、このようなメタデータを集める手段はほかにもあります。たとえば拙作のTest::ClassyではClass::Inspectorというモジュールを利用してテストメソッドの取得を行っていますし、先ほど名前をあげたTest::UnitではPerl内部のメタデータ用のハッシュを直接参照しています。

また、このようなメタ情報を扱うのであれば当然Moose(やClass::MOPの利用も検討されるべきでしょう。Mooseをテストに、というと起動コストを心配する方もおられるでしょうが、そもそもこのようなユニットテスト系のテストフレームワークを使いたくなるときはデータベースやモックサーバのように準備にそれなりの時間がかかるツールを併用することが多いので、テストフレームワーク自体の重さはそれほど気にする必要はありません(もちろんそのような道具立てを必要としない小さな非MooseモジュールのテストにMooseベースのテストフレームワークを利用するのは大げさすぎますが、そのあたりは常識で判断してください⁠⁠。

Mooseを利用したテストフレームワークはまだそれほど多くはありませんが、比較的知られているところでは2008年12月にリリースされたTest::Ableや2009年11月にリリースされたTest::Sweetといった例があります。いずれもまだ十分にこなれているとは言えず、情報量の少なさも相まって現時点ではあまりおすすめできるものではありませんが、Mooseを使ったアプリケーションを書いているなら利用を検討してみるのもよいでしょう。

ここでは参考までに先ほどのテストを両者を使って書き換えてみます。

Test::Able

Test::AbleのほうはTest::Classとかなり互換性があるので、それほど戸惑うことはないでしょう。本稿執筆時点ではテストプランの設定まわりにやや難があるのか、メソッド内にテストがひとつしか含まれない場合でも明示的にプランを指定する必要があるようでしたが、その問題と、Mooseの性質上単一ファイル内に処理をまとめる場合は実行部をあとに持ってくる必要がある、という問題を除けば、既存のTest::Classベースのテストはほとんどそのまま移植できます。

package MyTest;
use Test::Able;
use Test::More;
use DBI;

startup startup => sub {
    my $self = shift;
    $self->{dbh} = DBI->connect('dbi:SQLite::memory:');
    note "startup";
};

setup setup => sub {
    my $self = shift;
    $self->{dbh}->do('create table foo (id integer primary key, text)');
    note "setup";
};

test plan => 1, insert => sub {
    my $self = shift;
    ok $self->{dbh}->do('insert into foo values(?, ?)', undef, 1, 'my text');
};

test plan => 2, select => sub {
    my $self = shift;
    $self->insert;
    my ($text) = $self->{dbh}->selectrow_array('select text from foo where id = ?', undef, 1);
    is $text => 'my text';
};

teardown teardown => sub {
    my $self = shift;
    $self->{dbh}->do('drop table foo');
    note "teardown";
};

shutdown shutdown => sub {
    my $self = shift;
    $self->{dbh}->disconnect;
    note "shutdown";
};

package main;

MyTest->run_tests;

Test::Sweet

Test::Sweetのほうは連載第10回で紹介したMooseX::Declareを利用しているため、慣れるのが一苦労かもしれません。

Test::SweetはTest::AbleやTest::Classのようにstartup/setup/teardown/shutdownといった特殊なメソッドをサポートしていないため、ここでは同じことをするためにロール(トレート)をひとつ用意してみました。また、このロールの実装ではテストの内部で別のテストを呼ぼうとすると、そこでまたsetupやteardownが呼ばれておかしなことになってしまうため、insertテストも使い回す部分とそうでない部分にわけてあります(この例であれば使い回している部分はsetupメソッドのなかに入れてしまってもよいでしょう⁠⁠。

package MyTest;
use MooseX::Declare;

role Test::Sweet::Meta::Test::Trait::setup {
    around run($suite_class, @args) {
        $suite_class->setup;
        $self->$orig($suite_class, @args);
        $suite_class->teardown;
    }
}

class MyTest {
    use Test::Sweet;
    use DBI;

    method BUILD {
        $self->{dbh} = DBI->connect('dbi:SQLite::memory:');
        note "startup";
    };

    method setup {
        $self->{dbh}->do('create table foo (id integer primary key, text)');
        note "setup";
    }

    method teardown {
        $self->{dbh}->do('drop table foo');
        note "teardown";
    }

    method _insert {
        $self->{dbh}->do('insert into foo values(?, ?)', undef, 1, 'my text');
    }

    test insert (setup) {
        ok $self->_insert;
    }

    test select (setup) {
        $self->_insert;
        my ($text) = $self->{dbh}->selectrow_array('select text from foo where id = ?', undef, 1);
        is $text => 'my text';
    }

    method DEMOLISH {
        $self->{dbh}->disconnect;
        note "shutdown";
    }
}

package main;

MyTest->new->run;

サブテスト

ところで、このTest::Sweetのテストをperlで実際に実行してみると、やや見慣れない出力が得られることに気がつきます。

> perl test_sweet.t
# startup
1..2
    # setup
    ok 1
    # teardown
    1..1
ok 1 - insert
    # setup
    ok 1
    # teardown
    1..1
ok 2 - select
# shutdown

これはNested TAP(ネストしたTest Anything Protocol)と呼ばれる新しいTAPの表現方式で、構想そのものは2008年にオスロで開催された品質チームのハッカソンなどでも話し合われていたのですが、本格的に実装が始まったのは2009年になってからのこと。まだ一部に揺れている部分は残っているようですが、もっとも基本的なツールであるTest::Moreでは2009年6月リリースの0.89_01から開発者向けにsubtestというコマンドが公開されてNested TAPの恩恵を受けられるようになりました(この機能は2009年8月にリリースされたPerl 5.10.1には時期尚早ということで含められませんでしたが、同年9月にリリースされたTest::More 0.94で正式に公開されています⁠⁠。

同じテストをTest::Moreのsubtestを使って表現すると、おおよそこのような感じになるでしょうか。

use strict;
use warnings;
use Test::More 0.94;
use DBI;

my $dbh;

BEGIN {
    $dbh = DBI->connect('dbi:SQLite::memory:');
    note "startup";
}

subtest 'insert' => sub {
    setup();
    ok _insert();
    teardown();
    done_testing;
};

subtest 'select' => sub {
    setup();
    _insert();
    my ($text) = $dbh->selectrow_array('select text from foo where id = ?', undef, 1);
    is $text => 'my text';
    teardown();
    done_testing;
};

sub setup {
    $dbh->do('create table foo (id integer primary key, text)');
    note "setup";
}

sub teardown {
    $dbh->do('drop table foo');
    note "teardown";
}

sub _insert {
    $dbh->do('insert into foo values(?, ?)', undef, 1, 'my text');
}

done_testing;

END {
    $dbh->disconnect;
    note "shutdown";
}

本稿執筆時点ではサブテストひとつひとつにdone_testingやplan testsのような指定が必要なのがやや煩雑ですし、Test::Class系のツールのようにsetupやteardownの処理を自動化してくれるわけではないのでそのままユニットテストに使えるわけではありませんが、単純なテストフローの管理程度であれば、subtestでも十分用事は足ります。テストが複雑になってきたと感じたら、このようなツールを利用してファイルを分けたりコードブロックを分けたりすると、より見通しのよいテストが書けるようになります。

テストがないコードはレガシーコード

テストまわりのモジュールについてはほかにもまだ掘り下げておきたいものがありますが、連載第27回からかれこれ4回連続でテストまわりの話を続けてきたので、次回はまた別の系統の話に移ります。

どんなに新しい技術を使っても、テストのないコードはレガシーコードですので、特に新しくプログラマとして配属された方はもちろん、昔からPerlのコードを書いてきたベテランの方も、ぜひテストまわりのツールには目を光らせておいていただければと思います。

おすすめ記事

記事・ニュース一覧