Perlでもテスト
理解できない人には、なかなかその良さを理解してもらえないテストファーストですが、
私は結構好きです。
べつにテストファーストにこだわっている訳じゃないけれども、開発をしてれば簡単な
テストコードとか、動きを確認するための軽量なスタブなんかは作る訳だから、捨てる
ためのテストじゃなくって、後々まで使えたりコードとともに発展していくテストってのは
結構良いもんです。
あるのと無いのじゃ、安心感が違うしね。
そんなわけで、Perlでもテストができるようにしてみました。
まずは、プロジェクトの構成をこんな風にしてみます。
- 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を使用する
}
実は、あんまり詳しく調べてないから、使い方がよくわからないものも多かったり...