この記事は Perl入学式 Advent Calendar 2014 の 5日目です。
こんにちは。サポーターさせていただいてます まっすー(@trapple )です。
今日は昨日までと少し流れを変えて、普段のPerl入学式と同じような内容で行きたいと思います。
今年のPerl入学式の進行具合ですと、第4回サブルーチン/正規表現が終わったり終わらなかったりな進行具合だと思います。 今回はそのサブルーチンをちょっと発展させた内容になります。
サブルーチンまだ習ってないよ! or 忘れちゃったよ!って人はまずは講義資料に目を通してみてください。
それでは簡単な復習問題からスタートします。
- mon, tue, wedといった3文字英語表記の曜日を引数として受け取り、月曜日, 火曜日, 水曜日といった日本語3文字表記の曜日を返すサブルーチンtranslate()を作ってください。
- 標準入力から受け取った文字列をtranslate()に渡し、mon ならば 「mon は 月曜日 です」tue ならば 「tue は 火曜日 です」といった出力をするプログラムを作ってください。七曜以外の文字を受け取った場合は「変換できませんでした」と出力しましょう。
use strict;
use warnings;
use utf8;
binmode STDOUT, ':utf8';
chomp( my $input = <STDIN> );
if ( my $youbi_j = translate($input) ) {
print "$input は $youbi_j です\n";
} else {
print "変換できませんでした\n";
}
sub translate {
my $str = shift;
my %youbi = (
mon => '月曜日',
tue => '火曜日',
wed => '水曜日',
thu => '木曜日',
fri => '金曜日',
sat => '土曜日',
sun => '日曜日',
);
if ( exists $youbi{$str} ) {
return $youbi{$str};
} else {
return;
}
}
主にこれまで習ったことだけで出来てます。
exists
は ハッシュのキーが存在するかどうかを調べる関数でしたね。
入力値である$str
が%youbi
のキーmon
~fri
の中にあれば真を返します。
講義では、このサブルーチンtranslate()が正しく実装できているかを確認するコードを書きましょう、といった追加問題がありました。これがいわゆるテストです。 ifを駆使して以下のような感じで書くことができますね。
if( translate('mon') eq '月曜日' ) {
print "テストOK!\n";
}else{
print "テスト...\n";
}
これがテストの基本的な考え方です。
プログラミングの世界では、プログラムそのものと同じくらいテストが重要視される局面があります。 そのような需要に対応するために各言語それぞれテスト用の仕組みも充実しています。 PerlではTest::Moreというモジュールがそれになります。*1
それではそのTest::Moreを使ってテストを書いてみましょう。
あらたにテスト実行用のファイルyoubi.t
を作ります。Perlの世界ではテストファイルの拡張子は.t
とするのが一般的です。
そしてこのファイルに先ほどのyoubi.pl
を読み込んでみます。
# youbi.t
use strict;
use warnings;
use utf8;
require 'youbi.pl';
require
関数 が初登場しました。この関数に読み込むファイル名を指定すると、そのファイルの内容が実行されます。その結果youbi.t
の中でtranslate()
が呼び出せるようになります。
それではシェルでperl youbi.t
と打って実行してみましょう。
perl youbi.pl
を実行した時と同じように標準入力の待機状態になりました。
このままではテストがしづらいので小細工をします。
再びyoubi.plに戻って以下のようにしてください。
main() unless caller;
sub main {
chomp(my $input = <STDIN>);
if( my $youbi_j = translate($input) ) {
print "$input は $youbi_j です\n";
}else{
print "変換できませんでした\n";
}
}
translate()の定義以外の実行部分を新しいサブルーチンmain()として囲っています。
そしてそのサブルーチンをmain()で呼び出しているのですが、ここにunless caller
という条件が入っています。
caller
はPerlの組み込み関数で、現在の実行ファイルに対する、呼び出し元の情報を返します。
呼び出し元が無い場合は未定義値undef
を返します。日本語マニュアル
つまりyoubi.pl
がyoubi.t
から呼び出されている状態(テスト実行時)ならば、呼び出し元youbi.t
の情報が返ってくるのでmain()は実行しない。
perl youbi.pl
で実行していれば、呼び出し元は無いのでmain()
も実行される。
といった具合になります。
これで心置きなくテストからtranslate()が呼び出せます。
試しにテスト側でprint translate('mon')
など書いてみましょう。
次にTest::More
を読み込んで使ってみましょう。use Test::More;
を追加します。
# youbi.t
use strict;
use warnings;
use utf8;
use Test::More;
require 'calc.pl';
use
は内部的にはrequire
と同じ処理+αな機能を持ち、使い方の違いや初期化処理の追加などモジュール化を意識した作りになっています。
{重要}
PerlはCPANのモジュールを追加することで便利な機能を自由に追加できることはすでに説明があったとおもいますが、再利用や他人に使ってもらうことを意識したモジュールは、このuse
で使える形式で書く必要があります。
具体的には…
- 拡張子を
.pm
で作る。 package
を利用して名前空間を作る。- (必要に応じて)オブジェクト指向で記述する。
- (必要に応じて)
exporter
を使ってサブルーチンをエクスポートする。 require
とuse
の違いを意識する。- などなど
聞いたことがない用語が一気に出てしまったので、気になる人は調べてもらうことにしますが、
逆に言うとyoubi.pl
はuse
では読み込めない形式で書かれている = 再利用や他人に使ってもらうことをあまり考慮していないものだということを意識しておいてください。
例えば今回のケースで言うと、translate()
の部分を切り出して、モジュール化して使うなどが考えられます。その話はまた次のステップとしておきます。
{/重要}
本題に戻って...
Test::More
を読み込んだことによって色々な便利関数がインポートされています。その中からis()
を使って今回のテストを組み立ててみましょう。
is()
はis($one, $two, $three)
と3つの引数を受け取ります。
- $oneはテストしたい値
- $twoはテスト結果として期待される値
- $threeはテストの名前(省略可能)
# youbi.t
use strict;
use warnings;
use utf8;
use open ':std', ':encoding(utf8)';
use Test::More;
require 'youbi.pl';
is( translate("mon"), "月曜日", "mon => 月曜日" );
done_testing;
use open ':std', ':encoding(utf8)';
は日本語を使いたい時にuse Test::More;
の前に書いてください。
done_testing
はテストの終了を表す決まり文句としてつけるようにしてください。
出力結果
ok 1 - mon => 月曜日
1..1
okと出力されたらテスト成功です! 何個かテスト追記してみましょう。
is( translate("mon"), "月曜日", "mon => 月曜日" );
is( translate("tue"), "火曜日", "tue => 火曜日" );
is( translate("wed"), "水曜日", "wed => 水曜日" );
is( translate("thu"), "木曜日", "thu => 木曜日" );
is( translate("fri"), "金曜日", "fri => 金曜日" );
is( translate("sat"), "土曜日", "sat => 土曜日" );
is( translate("sun"), "日曜日", "sun => 日曜日" );
is( translate("hoge"), undef, "hoge => undef" );
出力結果
ok 1 - mon => 月曜日
ok 2 - tue => 火曜日
ok 3 - wed => 水曜日
ok 4 - thu => 木曜日
ok 5 - fri => 金曜日
ok 6 - sat => 土曜日
ok 7 - sun => 日曜日
ok 8 - hoge => undef
1..8
okが8つになりました。
ここでyoubi.plに機能を追加してみましょう。
内容はtranslate()
の引数に渡す文字がmon以外に,MonでもMONでもよい、というものです。
せっかくテストを書いているので、先に期待されるテスト内容を書いてしまいましょう。
is( translate("Mon"), "月曜日", "Mon => 月曜日" );
is( translate("MON"), "月曜日", "MON => 月曜日" );
出力結果
ok 1 - mon => 月曜日
ok 2 - tue => 火曜日
ok 3 - wed => 水曜日
ok 4 - thu => 木曜日
ok 5 - fri => 金曜日
ok 6 - sat => 土曜日
ok 7 - sun => 日曜日
ok 8 - hoge => undef
not ok 9 - Mon => 月曜日
# Failed test 'Mon => 月曜日'
# at youbi.t line 18.
# got: undef
# expected: '月曜日'
not ok 10 - MON => 月曜日
# Failed test 'MON => 月曜日'
# at youbi.t line 19.
# got: undef
# expected: '月曜日'
1..10
# Looks like you failed 2 tests of 10.
当然ですがテストが通りません。not ok
の部分です。期待される値は月曜日
なのにundef
が返ってますよ、とのことです。
ではこのテストが通るようにyoubi.pl
を改良してみましょう。
### 解答例
if( exists $youbi{lc $str} ){
return $youbi{lc $str};
}else{
return;
}
lcはPerlの組み込み関数で、大文字を小文字に変換します。 これでテストが通りました! このように、以前のテスト結果をキープしつつ新機能のテストをすれば、機能追加やロジックの再考(リファクタリング)が安全確実に行えるのが、テストを書くメリットの1つだと言われています。
- 標準入力からを
2014-12-5
といった日付を受け取り「2014-12-5 は 金曜日です」といった出力を返すプログラムに変更しましょう。 - 2014/12/5といった/を区切り文字にした日付も受け取れるようにしましょう。
Time::Pieceというモジュールを使ってみましょう。
こんな感じでしょうか?ポイントだけ説明しますと
入力内容の確認については、正規表現だけで行うと結構難しいので、Time::Piece->strptime()
のエラーを利用しました。
Time::Piece->strptime()
は指定した文字列フォーマットを受け取ってTime::Pieceオブジェクトを作ります。
エラーを取得する eval { hoge } if ($@) { fuga }
についてはevalのドキュメントを参考にしてください。
そのまま使えば良さそうだったので、このサブルーチンは変更がありません。 仮に変更がある場合は、内部ロジックの変更だけにとどめ出力は変わらないようにするのがベストでしょう。 テストはすでに書き終わっており実績があるので。 そうしないと、テストも全部書き直しになってしまいます(仕方ない場合もありますが)
以上、超テスト入門でした。 何かわからない部分などがあったらコメントに残してもらったり、Perl入学式に遊びに来て質問していただけると! Perl入学式は今回のように練習問題で手を動かしながら、その場で講師やサポーターに質問することも出来る場所です。
明日は飛び入りが無ければお休みで、明後日は tomcha_ さんです!
*1 Test::More 以外にも色々なテストモジュールがあります。それらを組み合わせるのが王道となっています。