Test::Moreでテスト事始めB!

はじめまして。

最近Perlを始めたmyfinderです。

ほかの方が書いたCPANモジュールを紹介したりそれらを使ったTipsを書くCasual Trackということで、今回はテストに関連するモジュールについて書きます。

プログラムの「テスト」

テストを書くというのはデンタルフロスを使うようなもので、誰もが良いことだといいつつ、実際には軽視されがちだ。

なんて言われることもありますが、実際テストがないとプログラムの改修とか引き継ぎとかが非常に大変になりがちです。

また、CPANにアップされているモジュールの中には、モジュール本体よりもテストコードの量の方が多いものも多々あります。

なのでプログラムの質を保証したり、内容を的確に伝達するにはテストが欠かせないものであることは間違いないと思います。

Test::More

Perlのプログラムをテストするための枠組みを提供するモジュールです。

多くのモジュールでこれが使われています。

Test::Moreは5.6.2からコアモジュールに入っているので、最近のPerl環境なら誰でもすぐにテストを始めることができます。

早速使ってみる

例えば消費税の計算をしてくれるConsumptionTax::JPというモジュールを仮定してみます。

このモジュールは、tax_includeという関数に価格を渡すと消費税を付加した価格を返してくれるだけの簡単なお仕事をするクラスです。

が、簡単なお仕事でも間違った値が返ってくると困ってしまいます。

例えば

  • 消費税込みの価格は原則小数点以下切り捨てなので、この関数が小数点を含む値を返してしまうことがあってはいけません。
  • 小数点が含まれた値を渡した場合、消費税の計算ができませんので、今回は入力された値も小数点以下を切り捨てて扱うこととします。

というようなことは仕様として明確にしておかなければならないでしょう。

今回はそういった「プログラムを書く前に決まる(っている)仕様」を押さえるテストを書いてみましょう。

use strict;
use warnings;

use Test::More;

plan (tests => 5);

# 正しくuseできるかどうか
use_ok("ConsumptionTax::JP");

my $consumption_jp = new ConsumptionTax::JP->new({
    consumption_tax_rate => 0.05,
});

# tax_includeという関数を実装しているか
can_ok($consumption_jp, 'tax_include');

# 小数点以下の出ない値
my $price = 100;

my $price_in_tax = $consumption_jp->tax_include($price);

is($price_in_tax, 105, '期待値と一致');

# 小数点以下が出る値
$price = 128;

$price_in_tax = $consumption_jp->tax_include($price);

is($price_in_tax, 134, '期待値と一致');

# 小数点が含まれる値
my $price_in_point = 100.50;

$price_in_tax = $consumption_jp->tax_include($price_in_point);

is($price_in_tax, 105, '期待値と一致');

テストが書けたら、まずテストを動かしてみましょう。

$ perl t/consumption_tax.t
1..5
not ok 1 - use ConsumptionTax::JP;
#   Failed test 'use ConsumptionTax::JP;'
#   at t/consumption_tax.t line 9.
#     Tried to use 'ConsumptionTax::JP'.
#     Error:  Can't locate ConsumptionTax/JP.pm in @INC (@INC contains: /Users/myfinder/local-lib/lib/perl5/darwin-2level /Users/myfinder/local-lib/lib/perl5 /Users/myfinder/local-lib/lib/perl5/darwin-2level /usr/local/lib/perl5/5.10.1/darwin-2level /usr/local/lib/perl5/5.10.1 /usr/local/lib/perl5/site_perl/5.10.1/darwin-2level /usr/local/lib/perl5/site_perl/5.10.1 .) at (eval 4) line 2.
# BEGIN failed--compilation aborted at (eval 4) line 2.
Can't locate object method "new" via package "ConsumptionTax::JP" (perhaps you forgot to load "ConsumptionTax::JP"?) at t/consumption_tax.t line 11.
# Looks like you planned 5 tests but ran 1.
# Looks like you failed 1 test of 1 run.
# Looks like your test exited with 255 just after 1.

当然のことながらテストに失敗します。

これから、このテストを満たすプログラムを実装していきます。

package ConsumptionTax::JP;

use strict;
use warnings;

use base qw/Class::Accessor::Fast/;

__PACKAGE__->mk_accessors(qw/ consumption_tax_rate /);

sub tax_include {
    my ($self, $price) = @_;
    return $price * (1 + $self->consumption_tax_rate);
}

1;

実装ができたら、早速テストを走らせます。

$ perl t/consumption_tax.t
1..5
ok 1 - use ConsumptionTax::JP;
ok 2 - ConsumptionTax::JP->can('tax_include')
ok 3 - 期待値と一致
not ok 4 - 期待値と一致
#   Failed test '期待値と一致'
#   at t/consumption_tax.t line 30.
#          got: '134.4'
#     expected: '134'
not ok 5 - 期待値と一致
#   Failed test '期待値と一致'
#   at t/consumption_tax.t line 37.
#          got: '105.525'
#     expected: '105'
# Looks like you failed 2 tests of 5.

おっと、どうやら実装したプログラムは「計算結果に小数点以下の数が出た場合」と「小数点以下が含まれた値が渡された場合」を考慮しない作りになっていたようです。

このように、期待値と一致しない場合には通知してくれます。

早速期待通り動作するように修正します。

return $price * (1 + $self->consumption_tax_rate);

となっているところを、下記のようにします。

return sprintf("%d", $price * (1 + $self->consumption_tax_rate));

修正したので、テストします。

$ perl t/consumption_tax.t
1..5
ok 1 - use ConsumptionTax::JP;
ok 2 - ConsumptionTax::JP->can('tax_include')
ok 3 - 期待値と一致
ok 4 - 期待値と一致
ok 5 - 期待値と一致

今度は期待通りの挙動をしています。

これで、仕様を満たすプログラムが実装できました。

その他のテスト用関数

今回はほぼisしか使いませんでしたが、他にも下記のようなものがよく使われたりします。

関数 何をチェックするか 使い方
ok 真偽値 ok($val, '$val is true');
isnt 文字列が等しいかどうか is($string, 'string', '$string is not "string"');
is_deeply arrayやhashの比較 is_deeply($val, { key => 'val'}, '$val is match');
like 正規表現と一致するかどうか like($val, qr/正規表現/, '$val is match');

他にもいろいろなテスト用関数が提供されています。

詳しく知りたい場合はperldocなどを確認するのがよいです。

テストが増えてきたら

開発しているモジュールの数が増えると、つられてどんどんテストファイルが増えていったりします。

そんなとき個別に

$ perl t/test.t

とかコマンドを打ちまくるのは刺身タンポポの香りがしてきます。

そんなときはproveコマンドを使うのがよいです。

proveコマンドはTest::Harnessについてくるコマンドラインツールです。

Test::Harnessもコアモジュールなので、特にインストールなどは不要です。

使い方は簡単で、

$ prove t/*.t

という風にコマンドを打つと、tディレクトリ以下にあるテストプログラムをまとめて実行してくれます。

おわりに

今回は新規にでっち上げたモジュールのテストを書きましたが、当然既存のモジュールに対するテストを記述することも可能です。

師走の大掃除の際にソースコードを整理することがあると思いますが、その際には是非テストコードを書いてあげてください。

明日はsfujiwaraさんです。楽しみにしています!

追記(done_testingについて)

xaicronさんに「done_testingに触れてほしかったなう」とご希望いただいたので追記します。

done_testingとはTest::Moreの0.87から追加された機能です。

通常テストを書く際には

plan (tests => 5);

という感じにテストの数を指定しなければなりません。

指定しない場合は「no_plan」と書くように言われているドキュメントもありますが、これだと本来通らないはずのテストが通ってしまったりして、テストの意味がなくなってしまいます。

とはいえ開発途上でテストの数がどんどん増えたり、あるいは減ったりすることはよくある話です。

そんなときにはno_planを指定せず、テストコードの最後に

done_testing;

と書いておきましょう。

そうしておくと、明示的にテスト数を指定しなくてもテストが実行でき、かつテストが失敗した場合はその旨表示してくれます。

SEE ALSO

http://d.hatena.ne.jp/tokuhirom/20090706/1246861746