Perlでもテスト

理解できない人には、なかなかその良さを理解してもらえないテストファーストですが、
私は結構好きです。

べつにテストファーストにこだわっている訳じゃないけれども、開発をしてれば簡単な
テストコードとか、動きを確認するための軽量なスタブなんかは作る訳だから、捨てる
ためのテストじゃなくって、後々まで使えたりコードとともに発展していくテストってのは
結構良いもんです。

あるのと無いのじゃ、安心感が違うしね。

そんなわけで、Perlでもテストができるようにしてみました。

まずは、プロジェクトの構成をこんな風にしてみます。

  1. MyModule

+ lib
+ MyModule
- Counter.pm
+ t
- MyModule.t

こういったサンプルを書くときには、簡単なカウンタークラスを作ることにしています。

今回想定した仕様としては、"Inc"メソッドでCounterクラスが内部に保持するカウンターを
インクリメントし、"Dec"メソッドでデクリメントするようなクラスを考えてみました。

内部の値は、"Get"メソッドを通して取得することにします。

そんなわけで、早速テストコードを書いてみることにします。

(恐らく)Perl文化の流儀に従って、テストコードは、"t"フォルダに入れて、拡張子は
"t"としてみることにしました。

(t/MyModule.t)

use utf8;
use strict;
use warnings;

use Test::More 'no_plan';
use MyModule::Counter;

my $a_counter = new MyModule::Counter();

# 構築のテスト
ok( $a_counter );
ok( $a_counter->Get() == 0 );

# インクリメントのテスト

$a_counter->Inc();
ok( $a_counter->Get() == 1 );

for( my $i = 0; $i < 20; $i++ ){
    $a_counter->Inc();
}
ok( $a_counter->Get() == 21 );


# デクリメントのテスト
$a_counter->Dec();
ok( $a_counter->Get() == 20 );

for( my $i = 0; $i < 10; $i++ ){
    $a_counter->Dec();
}
ok( $a_counter->Get() == 10 );

Test::Moreモジュールが、Perlでのテストを補助してくれるモジュールです。
細かな説明は、ヘルプをあたってもらうとして、簡単な使い方としては、"ok"に
テストのコードを書いていくだけです。

テストコードが書き終わったら、まず実行。
テストファーストで開発をしているので、まだCounterモジュールが存在しないって
おこられます。

user > perl -I ./lib ./t/MyModule.t
Can't locate MyModule/Counter.pm in @INC (@INC contains: ./lib /etc/perl /usr/local/lib/perl/5.8.8 /usr/local/share/perl/5.8.8 /usr/lib/perl5 \
/usr/share/perl5 /usr/lib/perl/5.8 /usr/share/perl/5.8 /usr/local/lib/site_perl .) at ./t/MyModule.t line 14.
BEGIN failed--compilation aborted at ./t/MyModule.t line 14.
# Looks like your test died before it could output anything.

そんなわけで、Counterモジュールの実装をしていきます。

(MyModule/Counter.pm)

use utf8;
use strict;
use warnings;

package MyModule::Counter;

sub new{
    my $this = shift;

    my $inst = {
                'count' => 0,
               };

    return bless $inst, $this;
}

sub Get{
    my $this = shift;

    return $this->{ 'count' };
}

sub Inc{
    my $this = shift;

    $this->{ 'count' }++;
    return $this->{ 'count' };
}

sub Dec{
    my $this = shift;

    $this->{ 'count' }--;
    return $this->{ 'count' };
}

1;

本当は、もう少し段階を追って、テストを進めた方が良いんでしょうけど、テスト技法の
解説じゃなくって、Perlでテストを行うときの解説なんでざっくり行きます。
(本来は、モジュールのファイルを用意して、実行して、メソッドの定義だけ行って、実行して
おこられて、Getを実装して....みたいな感じで、ひとつずつ解決していくのが正しいんでしょう)

そんなわけで、もう一回実行

user > perl -I ./lib ./t/MyModule.t
ok 1
ok 2
ok 3
ok 4
ok 5
ok 6
1..6

今度はちゃんとテストが通ったので、仕様を満たすCounterクラスが出来上がりました。

これで、安心して(?)開発がすすめられるってもんです。

(おまけ)

[テスト計画の宣言]

use Test::More tests => n; # テストをn個実行する
use Test::More 'no_plan'; # テストの個数を明らかにしない
use Test::More skip_all => $why; # 全てのテストをスキップする

[メソッド] -- $msgはテスト実行時に出力されるコメント、省略可

ok($cond, $msg); # $condが真ならPASS
is($var1, $var2, $msg); # $var1と$var2が同値ならPASS
isnt($var1, $var2, $msg); # $var1と$var2が同値でなければPASS
is_deeply($ref1, $ref2, $msg); # isと同様だがリファレンスの内部まで比較
like($var1, qr/$regexp/, $msg); # $var1が$regexpにマッチすればPASS
unlike($var1, qr/$regexp/, $msg); # $var1が$regexpにマッチしなければPASS
cmp_ok($var1, $op, $var2, $msg); # $var1 $op $var2が成立すればPASS
# $opは'<','=','>'などの比較演算子
isa_ok($obj, $class); # $objが$classに属していればPASS
can_ok($obj, $method); # $objが$methodを持っていればPASS
use_ok($module); # $moduleをロードできればPASS
# BEGINブロック内で使用する
require_ok($module); # $moduleをrequireできればPASS
pass($msg); # 無条件でPASS
fail($msg); # 無条件で失敗
BAIL_OUT($msg); # テストを中断する

[ブロック]

SKIP: { # $condが成立した場合、
skip $msg, $how_many if $cond; # ブロック内のテストをスキップする
...test codes... # $how_manyはブロック内のテストの個数
} # 特定のモジュールがインストールされていない場合
# それに依存するテストをスキップさせる等に使う

TODO: { # テストが失敗する事が分かっている場合に、
local $TODO = $msg; # ブロック内のテストがPASSしなくても
...test codes... # テスト全体の失敗とはならない
} # 未実装だがテストを先に書いて
# おきたい場合に使用する

TODO: { # TODOブロック内部は常に実行されるが、
todo_skip $msg, $how_many # 特定の条件が成立した場合に
if $cond; # テストをスキップさせたい場合は、
...test codes... # todo_skipを使用する
}

実は、あんまり詳しく調べてないから、使い方がよくわからないものも多かったり...