Perl入門ゼミ

  1. Perl
  2. 試験

Test::More - 自動テストのためのテストプログラムを書く

CPANにあるPerlモジュールのほぼすべてには、プログラムを試験するためのプログラムが、添付されています。プログラムだけではなくって、プログラムを試験するためのプログラムも一緒についてくるんです。これを、自動試験と呼びます。プログラムをひとつひとつ手で実行しなくても、自動試験のプログラムを実行するだけで、プログラムが正しく動くかどうかを確認できます。

Perlでは、標準モジュールであるTest::Moreを使って自動試験のプログラムを書くのが便利です。ここでは、自動試験の利点と、Test::Moreによる自動試験の記述方法を解説します。

自動試験の利点

1.リグレッションテストが簡単にできる。

リグレッションテストは、退行試験とも呼ばれ、プログラムに機能を追加したときに元の機能が正しく動くことを確認する試験のことです。自動試験を作成しておけば、プログラムに機能を追加した後に、自動試験が成功することを確かめればよいだけです。

2.リファクタリングができる

リファクタリングとはコードをきれいにすることです。汎用性、可読性があがるようにコードを書き直す行為をいいます。 汚れたコードをほうっておくのはよくありません。変化に対応しなければならないプログラムの場合は、機能の追加を行いやすいようなコーディングを心がける必要があります。

自動試験を作成しておけば、機能を損なうことなく、リファクタリングを行うことができます。

自動試験の注意点

1.コード自体の良い悪いはわからない。

自動試験は、機能を見るための試験です。そのコードが汎用性があるとか、きちんとサブルーチンにまとまっているとか、可読性が高いとか、そのような部分を判断することができません。インスペクション、コードリーディングなどと組み合わせればよりよいと思います。

2.目視による確認もあわせて行う

自動試験は完璧な試験ではありません。自動試験の書き間違えが発生します。勘違いで自動試験が行われていなかったとか、試験自体が間違えていたということもあります。

試験の間違いをなくすこつは、なるべく多角的な観点から試験をすることです。ですので、自動試験を書くときは、デバッガを使って、実際の動きを確認することを平行して行うことをお奨めします。プログラムの内部が意図したとおりに動いていることを確認しましょう。

正しい試験を一度書くことができれば、後は、自動で確認できます。

自動試験の簡単なサンプル

では早速、Perlの自動試験のためのモジュールTest::Moreを使って自動試験のプログラムを書いてみましょう。

use strict;
use warnings;

# テスト数を指定する
use Test::More tests => 2; 

# ok 関数で試験が成功したかを確認。この試験は成功する。
my $num1 = 1;
ok($num1 == 1); 

# この試験は失敗する
my $num2 = 2;
ok($num2 == 1);

自動試験のスクリプトの解説です。

1. 試験の数を指定する。

Test::Moreをuseするときはテストの数を指定します。

use Test::More tests => 2;

2. ok関数で試験の結果をチェック

ok関数の第1引数は、試験が成功する条件を記述します。

# ok 関数で試験が成功したかを確認。この試験は成功する。
my $num1 = 1;
ok($num1 == 1); 

# この試験は失敗する
my $num2 = 2;
ok($num2 == 1);

この記述は「$num1が1と等しいなら試験は成功」「$num2と1が等しいなら試験は成功」と読むことができます。

3. 試験結果

上記のサンプルの試験の結果は以下のようになります。1つ目の試験は成功、2つ目の試験は失敗したことがわかると思います。

1..2
ok 1 - Test1
not ok 2 - Test2
#   Failed test 'Test2'
#   at b.pl line 9.
# Looks like you failed 1 test of 2.

サブルーチンを試験する

では、もうひとつ簡単なサンプルを見てみましょう。二つの数を合計するsumという関数と数を2倍するdoubleという関数を試験します。double関数は2倍するはずが間違って3倍していて自動試験が失敗するようにしてあります。

use strict;
use warnings;

use Test::More tests => 2;


# 試験単位でスコープを作成するとmyで宣言したレキシカル変数の衝突が起きない。
# sumの試験
{
  my $num1 = 1;
  my $num2 = 2;
  
  my $total = sum($num1, $num2);
  
  ok($total == 3, 'sum');
}

# doubleの試験
{
  my $num1 = 1;
  
  my $double = double($num1);
    
  # 失敗!
  ok($double == 2, 'double'); 
}

# 二つの数を合計するサブルーチン
sub sum {
  my ($num1, $num2) = @_;
  return $num1 + $num2;
}

# 数を2倍するサブルーチン
sub double {
  my $num = shift;

  # 2倍しなければならないのに、間違って3倍してしまった!
  return $num * 3
}

1. サブルーチンの自動試験

自動試験をサブルーチンごとに行います。今回のサンプルでは、sumというサブルーチンを試験しています。

{
  my $num1 = 1;
  my $num2 = 2;
  
  my $total = sum($num1, $num2);
  
  ok($total == 3, 'sum');
}

# 二つの数を合計するサブルーチン
sub sum {
  my ($num1, $num2) = @_;
  return $num1 + $num2;
}

2. 試験単位にスコープを作成する。

試験をたくさん書いていくと、変数名の衝突が頻繁に起こるようになってきます。これを避けるために、{ } でスコープを作成して、その中にひとつひとつ試験を書いていきます。

# sumの試験
{
  my $num1 = 1;
  my $num2 = 2;
  
  my $total = sum( $num1, $num2 );
  
  ok( $total == 3, 'sum' );
}

# doubleの試験
{
  my $num1 = 1;
  
  my $double = double($num1);
  
  ok($double == 2, 'double'); # 失敗!
}

3. 試験結果

試験結果を見てみましょう。double関数は間違って、3倍する関数になっていますので、2番目の試験が失敗します。

ok 1 - sum
not ok 2 - double
#   Failed test 'double'
#   at - line 25.
# Looks like you failed 1 test of 2.

自動試験のレイアウト

上記のスクリプトは、ひとつのファイルの中で行いましたが、自動試験のディレクトトリの一般的なレイアウトを紹介します。自動試験のディレクトリのレイアウトは以下のようになります。SomeModule.pmというモジュールを試験することを想定します。

SomeModule/ 
    |
    |---- lib/
    |      |
    |      |----SomeModule.pm
    |
    |
    |----- t/
           |
           |---- sum.t
           |
           |---- double.t

モジュール自体は、libというディレクトリの中に格納します。自動試験のプログラムは、t という名前のディレクトリの中に格納します。自動試験のプログラムの拡張子は .t にします。

レイアウトに絶対的な決まりはありませんが、Perlの自動試験を便利にしてくれるモジュールの多くがこのレイアウトを想定しているので、このレイアウトにしましょう。

モジュールのサンプル

試験をするためのモジュールのサンプルです。「SomeModule.pm」という名前で保存して、「lib」の中に格納します。

package SomeModule;
use strict;
use warnings;

# 二つの数を合計する関数
sub sum {
  my ($num1, $num2) = @_;
  return $num1 + $num2;
}

# 数を2倍するサブルーチン
sub double {
  my $num = shift;

  # 2倍しなければならないのに、間違って3倍してしまった!
  return $num * 3
}

1;

モジュールの最後の行は真の値を返す必要があります。忘れやすいので注意しましょう。

試験スクリプト

自動試験のサンプルです。

sum.t

use strict;
use warnings;

# モジュールの検索パスを追加
use FindBin;
use lib "$FindBin::Bin/../lib";

use Test::More tests => 1;
use SomeModule;

# sumの試験
{
  my $num1 = 1;
  my $num2 = 2;
  
  my $total = SomeModule::sum($num1, $num2);
  
  ok($total == 3, 'sum');
}

(参考)FindBin

double.t

use strict;
use warnings;

# モジュールの検索パスを追加
use FindBin;
use lib "$FindBin::Bin/../lib";

use Test::More tests => 1;
use SomeModule; 

# doubleの試験
{
  my $num1 = 1;
  
  my $double = SomeModule::double($num1);
  
  ok($double == 2, 'double');
}

自動試験の実行

自動試験を実行して見ましょう。自動試験を行うには、まずSomoModuleの中に移動します。そして、次のコマンドを実行してみましょう。

perl t/sum.t

試験を実行することができます。

proveスクリプトですべての自動試験を実行する

自動試験のプログラムが増えてくると一つ一つ自動試験を実行するのは非常に面倒です。Perlにはproveというすべての自動試験を実行してくれる便利なスクリプトがあります。

prove t

proveを使うと、すべての自動試験を実行してくれるのでとても便利です。

proveの実行結果は以下のようになります。ひとつめの試験が成功して、二つ目の試験が失敗していることが確認できます。

t\sum.......ok
t\double....
t\double....NOK 1/1#   Failed test 'double'
#   at t\double.t line 13.
# Looks like you failed 1 test of 1.
t\double....dubious
        Test returned status 1 (wstat 256, 0x100)
DIED. FAILED test 1
        Failed 1/1 tests, 0.00% okay
Failed Test   Stat Wstat Total Fail  List of Failed
-----------------------------------------------------------------------------
t\double.t    1   256     1    1  1
Failed 1/2 test scripts. 1/2 subtests failed.
Files=2, Tests=2,  0 wallclock secs ( 0.00 cusr +  0.00 csys =  0.00 CPU)
Failed 1/2 test programs. 1/2 subtests failed.

double関数が間違っていますので数値を2倍するように修正すると、以下のようにすべての試験が成功します。

t\sum.......ok
t\double....ok
All tests successful.
Files=2, Tests=2,  0 wallclock secs ( 0.00 cusr +  0.00 csys =  0.00 CPU)

Test::Moreの関数

これまでは「ok関数」だけを使って、試験を作成してきましたが、Test::Moreモジュールには、たくさんの便利な関数があります。

真偽値、数値、文字列、オブジェクトの中身などを試験するには、以下の関数を使用します。

関数 解説
is eqを使っての文字列が等しいことを確認。整数の数値比較はこれでもOK
isnt neを使って文字列が等しくないことを確認。整数の数値比較はこれでもOK
like 文字列が正規表現にマッチすることを確認
unlike 文字列が正規表現にマッチしないことを確認
cmp_ok ==, <, >, <=, >=, gt, lt, などPerlの演算子を指定して比較
is_deeply 配列の配列など複雑なデータ構造の比較に用いる

is関数とisnt関数

is関数は文字列が等しいことを試験します。isnt関数は文字列が等しくないことを試験します。

is($str, 3);
isnt($str, 5);

第1引数は、試験を行いたい値です。第2引数は予想される結果です。

is と isnt は、eq と ne を使用するので文字列としての比較になります。(たとえば "12.0" と 12 は異なります。) 数値を比較する場合で文字列として比較しても問題ないような場合は、 is と isnt を使っても良いです。

それ以外の場合は、 cmp_ok という関数を使用します。

like関数とunlike関数

like関数は文字列が正規表現にマッチすることを試験します。unlike関数は正規表現にマッチしないことを試験します。

like( $str, qr/\w+/);
unlike( $str, qr/\w+/);

第1引数は試験したい値です。第2引数は、正規表現のリファレンスです。

第2引数は、qr/\w+/ という正規表現のリファレンスになっていることに注意してください。これを '\w+' や、 /\w+/ のようにすると、試験がうまくいきません。

cmp_ok関数

cmp_ok関数を使うと任意の比較演算子を使って試験ができます。

cmp_ok( $num, '<', 3); 

第1引数、第3引数は比較したい値です。第2引数は比較演算子です。「=」「<」「<=」「>=」「>」「eq」「ne」「gt」「lt」「ge」「le」など、すべての比較演算子が使用できます。

is_deeply関数

is_deeply関数を使うと、配列のリファレンスやハッシュのリファレンスなどの複雑なデータ構造の試験を行うことができます。これはとても便利です。

is_deeply($hash_ref, {a => 1, b => 2});

第1引数は、試験をしたい値です。第2引数は、予想される結果です。

次に、モジュールの読み込み、クラスの定義などを試験するための関数を紹介します。

can_ok クラスがあるメソッドを持つかどうかの確認
isa_ok オブジェクトが継承しているクラスの確認、リファレンスの種類の確認
require_ok モジュールがrequireできることを確認
use_ok モジュールがuseできることを確認

can_ok関数

can_ok関数は、あるクラスがあるメソッドを持っていることを確かめるための試験に使用します。

can_ok('Book', 'title', 'author', 'price', 'to_string', 'clone');

これは、Bookクラスが、title, author, price, to_string, clone というメソッド持っていることを確認するための試験です。

isa_ok関数

isa_okはあるオブジェクトがあるクラスを継承していることを確かめるための試験に使用します。

my $comic = Comic->new;
isa_ok($comic, 'Book');

これは、$comicオブジェクトがBookクラスを継承していることを確かめるための試験です。

以下のように自分自身のクラスを指定しても試験は成功します。

isa_ok($comic, 'Comic');

また、オブジェクトではなくて、リファレンスがどのような種類ものものであるかの試験にも対応しています。

my $ary_ref = [];
isa_ok($array_ref, 'ARRAY');

require_ok関数

require_ok関数はモジュールがrequireできることを確認するための試験に使用します。

require_ok('Book');

これは、Bookモジュールがrequireできることを確認する試験です。require_okの使い道ですが、自分が配布したいモジュールが、別のモジュールに依存している場合、そのモジュールが存在することを確認する場合などに利用できます。

use_ok関数

use_ok関数はモジュールがuseできることを確認するための試験に使用します。

BEGIN{ use_ok('Some::Module') }

FAQ

試験結果にわかりやすいコメントを表示することはできますか

note関数を使用すると、試験結果にコメントを表示することができます。

note 'some test';

試験の数を記述するのがめんどくさいです

「no_plan」あるいは「done_testing」を使用します。

use Test::More 'no_plan';
use Test::More;

# 試験を書く

done_testing;

現在では、done_testingが推奨されています。

まれに、試験が失敗しているのに、通るエッジケースがあるようです。確実性を求めるのであれば、テスト数を記述しましょう。

一部の試験をスキップしたいです

たとえば、特定の環境では、試験をスキップしたいという場合がありますね。次のように記述します。

use Test::More;
if ($^O eq 'MacOS') {
  plan skip_all => 'Test irrelevant on MacOS';
}
else {
  plan tests => 42;
}

例外の試験はどのように行いますか。

例外が起こることを確認したい場合は、次のようにします。

# 例外が起こることの確認
eval { some_func() };
if ($@) {
  ok(1);
}
else {
  ok(0);
}

# あるいはエラーメッセージを確認
eval { some_func() };
like($@, qr/error/);

(参考)eval

警告の試験はどのように行いますか。

警告がでることを確認したい場合は、シグナルハンドラを利用して、次のようにします。

# 警告メッセージを代入するために準備
my $warn;

# 警告が発生したときに呼び出されるハンドラ
local $SIG{__WARN__} = sub {
  $warn = shift;
};

$warn = undef;
some_func();
if ($warn) {
  ok(1);
}
else {
  ok(0);
}

最後に

Test::Moreを使って自動試験を作成しておくと、自動試験を実行するだけで、プログラムが正しいかどうかを確認できるので便利です。また、CPANモジュールを自分で書くためには、必ず覚える必要があります。

僕が、試験を自動化できるということを知ったときは、そんな便利な方法があったんだと目からうろこでした。みなさんも、便利に利用してみてください。