tsucchi’s diary(元はてなダイアリー)

はてなダイアリー(d.hatena.ne.jp/tsucchi1022)から移行したものです

オプション解析をテストする

前に書いたように、レガシーコードを再構成する場合、テストコードを書かないと容易に死んでしまえます。それじゃあ困るので、テストコードをちゃんと書きます。

お題は前回の my_prog.pl を使います。最終ゴールも前回のコードとします。

#!/usr/bin/perl -w

$a_str = undef;

read_args();
print "-a オプションで渡された値 : $a_str\n" if ( defined $a_str );
# 本当は main な処理が続く...

sub read_args {
  while( $_ = shift @ARGV ) {
    if ( $_ =~ /^-h$/ ) {
      usage();
    }
    elsif ( $_ =~ /^-a$/ ) {
      $a_str = shift @ARGV;
      if ( ! defined $a_str ) {
        usage();
      }
    }
  }
}

sub usage {
  print "my_prog.pl [-h] [-a somestring]\n";
  exit;
}

まず、このままじゃ使えないので、とりあえずモジュール化します。main 処理を省き、package 化します。名前も変えちゃいましょう。

% cp my_prog.pl my_prog.pm

my_prog.pm を編集します。

#!/usr/bin/perl -w
package my_prog;
$a_str = undef;

#read_args();
#print "-a オプションで渡された値 : $a_str\n" if ( defined $a_str );
## 本当は main な処理が続く...

sub read_args {
  while( $_ = shift @ARGV ) {
    if ( $_ =~ /^-h$/ ) {
      usage();
    }
    elsif ( $_ =~ /^-a$/ ) {
      $a_str = shift @ARGV;
      if ( ! defined $a_str ) {
        usage();
      }
    }
  }
}

sub usage {
  print "my_prog.pl [-h] [-a somestring]\n";
  exit;
}
1;

実際の作業ならコメントアウトじゃなくて削除でもいいです。

ではとりあえず、コレのテストコードを書きます。
まずは簡単そう(?)な -h オプションのテストコードを書きます

#!/usr/bin/perl

use strict;
use warnings;

ReadArgsTest->runtests;

package ReadArgsTest;
use base qw(Test::Class);

use my_prog;
use Test::More "no_plan";

sub h_option_test : Test(1) {
  {
    local @ARGV = ("-h");
    local *STDOUT;

    open(STDOUT, '>', 'stdout.txt');
    my_prog::read_args();
    close(STDOUT);
    is(_stdout(), "my_prog.pl [-h] [-a somestring]\n");
  }
}

sub _stdout {
  open(my $stdout_file, '<', 'stdout.txt');
  my $data = do { local $/; <$stdout_file> };
  close($stdout_file);
  return $data;
}

sub setup : Test(setup) {
  unlink('stdout.txt') if -e 'stdout.txt';
}

で、コレを実行!

% prove 01_my_prog.t
01_my_prog....# No tests run!
01_my_prog.... Dubious, test returned 255 (wstat 65280, 0xff00)
 No subtests run 

Test Summary Report
-------------------
01_my_prog (Wstat: 65280 Tests: 0 Failed: 0)
  Non-zero exit status: 255
  Parse errors: No plan found in TAP output
Files=1, Tests=0,  0 wallclock secs ( 0.00 usr  0.05 sys +  0.01 cusr  0.08 csys =  0.14 CPU)
Result: FAIL
Failed 1/1 test programs. 0/0 subtests failed.

あれ? No tests run です...。

これ、すごく悩みました。何がダメかというと、exit なんです。テストする前に exit しちゃうから。

もし、exit を書き換えることができないのなら、多分あきらめるしかないです。僕の場合は print + exit を die に置き換えても動作上問題なかったので、そうしました。

die に変えると、テストコードもかなり簡単に書けます。

まず本コード(usage だけ抜粋)

sub usage {
  die "my_prog.pl [-h] [-a somestring]\n";
}

次にテストコード

#!/usr/bin/perl

use strict;
use warnings;

ReadArgsTest->runtests;

package ReadArgsTest;
use base qw(Test::Class);

use my_prog;
use Test::More "no_plan";

sub h_option_test : Test(1) {
  {
    local @ARGV = ("-h");

    eval { my_prog::read_args();  };
    
    is($@, "my_prog.pl [-h] [-a somestring]\n");
  }
}

STDOUT を無理矢理捕まえる必要が無くなったので、だいぶ良くなりました。実行してみます。

% prove 01_my_prog.t 
01_my_prog....ok   
All tests successful.
Files=1, Tests=1,  0 wallclock secs ( 0.00 usr  0.05 sys +  0.02 cusr  0.07 csys =  0.14 CPU)
Result: PASS

通りました。

では他のオプションのテストコードも書きます。
こんな感じになりました。

#!/usr/bin/perl

use strict;
use warnings;

ReadArgsTest->runtests;

package ReadArgsTest;
use base qw(Test::Class);

use my_prog;
use Test::More "no_plan";

sub h_option_test : Test(2) {
  {
    local @ARGV = ("-h");

    eval { my_prog::read_args();  };

    is($@, "my_prog.pl [-h] [-a somestring]\n");
    ok( !defined $my_prog::a_str);
  }
}

sub a_option_no_arg_test : Test(2) {
  {
    local @ARGV = ("-a");

    eval { my_prog::read_args();  };

    is($@, "my_prog.pl [-h] [-a somestring]\n");
    ok( !defined $my_prog::a_str);
  }
}

sub a_option_with_arg_test : Test(2) {
  {
    local @ARGV = ("-a", "a_value");

    eval { my_prog::read_args();  };

    is($@, "");
    is($my_prog::a_str, "a_value");
  }
}

sub setup : Test(setup) {
  $my_prog::a_str = undef;
}

もっと書きたいのですが、とりあえず今日はこれでおしまい。次回は my_prog.pm をリファクタリングしていきます。(予定)