perlの単体テスト

概要

ちょっと用があって、perlでプログラムを書くことになりました.
少し規模感のあるプログラムなので、単体テストが無いと不安だなぁ…等と思い立って、perlでの単体テストについて調べてみました.
せっかくなのでメモを公開しておきます.

【目次】

  • Test::Simple と Test::More がある
  • フォルダ構成の約束事
  • Counter.pm の仕様を決める
  • テストの書き方
  • テストの実行1
  • モジュールの作成
  • テストの実行2
  • テストスイートの実行
  • まとめ
  • 参考書籍

Test::Simple と Test::More がある

perl の標準的な単体テストモジュールには、"Test::Simple"と"Test::More"がある.
"Test::Simple"は簡単なテストを、"Test::More"は詳細な(?)テストを行うものらしいが、より機能が多い方を学んでおけば今後に良いだろうと言うことで、今回は"Test::More"を使ってみることにした.

"Test::More"モジュール自体は、cpanからインストールすれば良い.

フォルダ構成の約束事

テストのファイルは、package や実行ファイル(*.pl)と同列に"t"フォルダを置き、その配下にテストのファイルを置くのが習わしとなっているらしい.
そんなわけで、次のようなフォルダ構成を想像してみることにした.

+$project
  -$code.pl
  +$package
    -$module.pm
    +t
      -$module.t

例示を交えた方が書きやすそうなので、サンプルとして数を数えるだけのモジュール Counter.pm を作成するとして、構成は次の様にすることとした.

+test
  -sample.pl
  +ilv
    -Counter.pm
    +t
      -Counter.t


パッケージ名は適当に"ilv"としておく.

Counter.pm の仕様を決める

サンプルなので

$counter = new ilv::Counter( 0 );

としてカウンターオブジェクトを作成して、次のように inc /dec でカウンタを上げ下げ出来れば良いとしておく.

$counter->inc();
print $counter->get() . "\n"; # 1 と表示される
$counter->dec();
print $counter->get() . "\n"; # 0 と表示される

ま、簡単なカウンタークラス.

テストの書き方

TestFirstって言葉もあるように、モジュールの実装前にテストを書く.
全体像を先に示してしまうと以下になる.

(counter.t)

#!/opt/local/bin/perl# -*- mode: perl; coding: utf-8-unix; tab-width: 4; -*-
use utf8;
use strict;
use warnings;
use Test::More qw(no_plan);
use ilv::Counter;

my $counter = new ilv::Counter( 0 );

is( $counter->get(), 0, "Construct");
is( $counter->toString(), "Count = 0", "toString" );

$counter->inc();
is( $counter->get(), 2, "inc" );

$counter->dec();
is( $counter->get(), 0, "dec" );

$counter->dec();
is( $counter->get(), -1, "negative value" );
is( $counter->toString(), "Count = -1", "toString" );
#
# EOF
#

補足を入れておくと

use Test::More qw(no_plan);

この部分は、Test::More モジュールの利用時にテストの件数を指定するらしい.
"no_plan" と指定すると件数の指定が無い状態でテストが実行される.
恐らく全てのテストが終了したことを示すためには件数を指定しておいた方が良いんだろうけど、めんどくさいので今回はパスすることにした.

肝心のテスト項目は、関数"is()"を使って書いていく.
第一引数にテストする値、第二引数に期待する値、第三引数にテストの名称をいれるものらしい.

テストの実行

実行は、普通のperlプログラムよろしく

> perl ./ilv/t/Counter.t


と実行すればよい.
当然、モジュールは存在しないので次のようなエラーが出力される.

Can't locate ilv/Counter.pm in @INC (@INC contains: /opt/local/lib/perl5/site_perl/5.8.9/darwin-2level /opt/local/lib/perl5/site_perl/5.8.9 /opt/local/lib/perl5/site_perl /opt/local/lib/perl5/vendor_perl/5.8.9/darwin-2level /opt/local/lib/perl5/vendor_perl/5.8.9 /opt/local/lib/perl5/vendor_perl /opt/local/lib/perl5/5.8.9/darwin-2level /opt/local/lib/perl5/5.8.9 .) at ./ilv/t/Counter.t line 8.
BEGIN failed--compilation aborted at ./ilv/t/Counter.t line 8.
# Looks like your test died before it could output anything.

モジュールの作成

さて、ここからは実際にモジュールを作成する.

# -*- mode: perl; coding: utf-8-unix; tab-width: 4; -*-#
# class 利用のサンプルとしてのカウンター
#
package ilv::Counter;
use utf8;
use strict;

# コンストラクタ
sub new{
    my $this = shift;

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

    return bless $inst, $this;
}

# インクリメント
sub inc{
    my $this = shift;
}

# デクリメント
sub dec{
    my $this = shift;
}

# 値を得る
sub get{
    my $this = shift;
}

1;

perl でクラスを利用する時の基本形みたいなもの.
関数のそれぞれの頭で "my $this = shift;" としているのは、

my $counter = new ilv::Counter( 0 );
$counter->get();

のようにアロー演算子を利用して関数呼び出しする時に、thisポインタを取り出すためだと認識している.
shift 関数自体は、配列の先頭要素を取り出す機能を持っているので、

get($counter);

としているようなモノだと考えてしまって良さそう.
なんとなく、Cの構造体を利用してクラスのようなことを実現する感じに似ている…

ほら、

struct Inst{
    int count;
};

void inc( Inst* i_this )
{
    i_this->count++;
}

こんな感じに書くでしょ?
そういったように、コンストラクタを眺めると、

# コンストラクタ
sub new{
    my $this = shift;
    my $inst = {
        'count' => 0,
    };
    return bless $inst, $this;
}


メンバ変数に当たる部分はハッシュテーブルとして確保しておき、bless で $this と $inst を結びつけるのがイメージしやすいと思う.
テストの説明を先に進めるために、関数は空にしたまま実装しておく.

テストの実行2

そして再度テストの実行をすると、今度はモジュールが存在するのでテストに失敗することがわかる

> perl ./t/Counter.t
not ok 1 - Construct
#   Failed test 'Construct'
#   at ./ilv/t/Counter.t line 13.
#          got: 'ilv::Counter=HASH(0x1008001f0)'
#     expected: '0'
ok 2 - toString
not ok 3 - inc
#   Failed test 'inc'
#   at ./ilv/t/Counter.t line 18.
#          got: 'ilv::Counter=HASH(0x1008001f0)'
#     expected: '2'
not ok 4 - dec
#   Failed test 'dec'
#   at ./ilv/t/Counter.t line 22.
#          got: 'ilv::Counter=HASH(0x1008001f0)'
#     expected: '0'
not ok 5 - negative value
#   Failed test 'negative value'
#   at ./ilv/t/Counter.t line 26.
#          got: 'ilv::Counter=HASH(0x1008001f0)'
#     expected: '-1'
not ok 6 - toString
#   Failed test 'toString'
#   at ./ilv/t/Counter.t line 27.
#          got: 'Count = 0'
#     expected: 'Count = -1'
1..6
# Looks like you failed 5 tests of 6.

眺めると何となく意味はわかると思うが、"got:"と書かれている部分がテストする値で、"HASH(xxxx)"となっているのはテスト対象としているget関数が戻り値を正確に返していないことが原因.

あとは、テストが通るようにモジュールを修正すると、

> perl ./t/Counter.t
ok 1 - Construct
ok 2 - toString
ok 3 - inc
ok 4 - dec
ok 5 - negative value
ok 6 - toString
1..6

こんな感じで正常なテスト結果が得られる.

テストスイートの実行

複数のテストをまとめて実行するために、テストスイートが用意されている.
使い方は簡単で、次のように実行すればよい.

> prove -r
ilv/t/Counter....ok
All tests successful.
Files=1, Tests=6,  0 wallclock secs ( 0.01 cusr +  0.00 csys =  0.01 CPU)

prove というのが、指定されたテストをまとめて実行するためのコマンドとなっていて、"-r"オプションを付けるとフォルダを再帰的に検索してテストを探して実行してくれるモノらしい.

まとめ

perlに限らず単体テストをつかった開発ってのは、経験してみないと効力はわかりづらい気がする.
なれてくると、テストを書かずに開発を進めるのに抵抗を感じるようになってくるんだけれども…

ともあれ、perl単体テストの方法や、テストスイートの実行方法を学びました.
あと、久しぶりのperlということで、さらっとモジュールの作り方の復習もおこないました.

参考書籍


Perlベストプラクティス

Perlベストプラクティス

perl単体テストについて調べていたらこの本について取り上げているエントリーを見つけて購入しました.
18章に「テストとデバッグ」の章が設けられていたので参照しました.
この本を読むまで、テストの置き場所というかフォルダ構成がよく分からなくて困っていたので参考になりました.
"t"フォルダにテストを置くのは少し座りが悪い感じがするのだけれども...
『ベストプラクティス』らしいので素直にしたがうことにしようと思ったのです.

...エントリーとは関係ないけれども...
本書は、オライリーEbook storeから購入しました.

http://www.oreilly.co.jp/ebook/#all_titles

pdfが直接買えるってのは楽で良いわ

以上