Perlモジュール作成実況中継 一気通貫 テストを添えて
Perl でモジュール作るなら minilla がおすすめ(というかそれしか知らない)
Perl でモジュールを作るときは Minilla
を使っています。
きっかけは Web+DB Press に掲載されて記事でした。
体系だった記事があると大変助かります。
$ cpanm Minilla
でインストール後、早速モジュール作ります。
作るのは Acme::MetaVar
というモジュール。
このモジュールは use Acme::MetaVar
すると、hoge
というメソッドを呼び出して使うことができる・・・とします。
利用イメージとしてはこんな感じ。
#!/usr/bin/env perl use strict; use warnings; use Acme::MetaVar; my $am = Acme::MetaVar->new(); print $am->hoge() . "\n"; # hoge
minil
の使い方については記事を見てもらうとして、こんな感じで進んでいきます。
$ minil new Acme::MetaVar (中略) Finished to create Acme::MetaVar
これで雛形が出来上がったので、早速モジュール本体を作っていきます・・・の前に、git でコミットしておきます。
$ cd Acme-MetaVar/ $ git add . $ git commit -m "initial commit" [master (root-commit) 8704b2c] initial commit 11 files changed, 585 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Build.PL create mode 100644 Changes create mode 100644 LICENSE create mode 100644 META.json create mode 100644 README.md create mode 100644 cpanfile create mode 100644 lib/Acme/MetaVar.pm create mode 100644 minil.toml create mode 100644 t/00_compile.t
ここまでの進捗です。
いざモジュール作成
モジュール作成に着手します。
$ cd Acme-MetaVar/ $ vim lib/Acme/MetaVar.pm
雛形のコードにサブルーチン9行足しただけですが、コードを載せておきます。
package Acme::MetaVar; use 5.008001; use strict; use warnings; our $VERSION = "0.01"; sub new { my $class = shift; my $self = bless {}, $class; return $self; } sub hoge { return "hoge"; } 1; __END__ (中略)
さて、出来上がったら -c
オプションつけてまずは文法チェックしてみます。
$ perl -c lib/Acme/MetaVar.pm lib/Acme/MetaVar.pm syntax OK
問題ないっすね。
ここまでの進捗です
テストしてみる
ここで、Minilla
が用意した雛形のテストを実行します。モジュールが use
できるかどうか?というテストです。
テストには Perl と一緒にインストールされる prove
コマンドを用います。
この prove
は t/
ディレクトリ内のテストスクリプトを連続で実行してくれます。
実行場所は t/
ディレクトリと同じ階層になるので注意。t/
ディレクトリの中ではないです。
引数の -l
を忘れずに。
$ prove -l t/00_compile.t .. ok All tests successful. Files=1, Tests=1, 1 wallclock secs ( 0.04 usr 0.01 sys + 0.14 cusr 0.03 csys = 0.22 CPU) Result: PASS
Result: PASS
、最高ですね。テスト成功です。
-l
オプションをつけないと、「Perlがモジュール見つけられないよ!」ってな警告と共にテストが失敗します。
Perl がモジュールを探す場所(パス)に Acme::MetaVar
が入っていないので当然ですね。今作ってる最中でインストール前だし・・・
以下がテスト失敗時のメッセージです。
$ prove t/00_compile.t .. 1/? # Failed test 'use Acme::MetaVar;' # at t/00_compile.t line 4. # Tried to use 'Acme::MetaVar'. # Error: Can't locate Acme/MetaVar.pm in @INC (you may need to install the Acme::MetaVar module) (中略・ここにPerlがモジュールを探したパスが列挙されている) at t/00_compile.t line 4. # BEGIN failed--compilation aborted at t/00_compile.t line 4. # Looks like you failed 1 test of 1. t/00_compile.t .. Dubious, test returned 1 (wstat 256, 0x100) Failed 1/1 subtests Test Summary Report ------------------- t/00_compile.t (Wstat: 256 Tests: 1 Failed: 1) Failed test: 1 Non-zero exit status: 1 Files=1, Tests=1, 1 wallclock secs ( 0.03 usr 0.01 sys + 0.14 cusr 0.04 csys = 0.22 CPU) Result: FAIL
ではここで一旦 git add .
して git commit
です。
sironekotoro 20-09-06 14:38:39 ~/Dropbox/perl/temp/Acme-MetaVar (master +) $ git commit -m "add method 'new', 'hoge'" [master 79c4048] add method 'new', 'hoge' 1 file changed, 9 insertions(+), 1 deletion(-)
閑話休題、実行時のパスについて
さて、スクリプトから開発中のモジュールを use
して使って試してみたいときにも、同様にパスが見つからないよ!ってな問題が発生します。
例えば、Acme-MetaVar/
に test.pl
を作って use
してみます。
$ touch test.pl
test.pl
の中身です。use
しただけ。
#!/usr/bin/env perl use strict; use warnings; use Acme::MetaVar;
実行するとエラーが出ます。prove
を -l
なしで実行した時と同じですね。
$ perl test.pl Can't locate Acme/MetaVar.pm in @INC (you may need to install the Acme::MetaVar module) (中略) at test.pl line 5. BEGIN failed--compilation aborted at test.pl line 5.
ですので、モジュール作成中のテストスクリプト中では、モジュールの位置を明示してあげる必要があります。
この test.pl
からみてモジュール Acme::MetaVar
があるのは同じ階層にある ./lib
以下です。ですので、このように1行追加します。
#!/usr/bin/env perl use strict; use warnings; use lib ('./lib'); use Acme::MetaVar;
これでスクリプトを実行してもエラーが出なくなりました。
$ perl test.pl
テスト用のスクリプトを別の階層に書く場合は、相対パスにしろ絶対パスにしろ、モジュールがある場所を指定する必要があります。
テストを追加する
では、引き続きテストスクリプトを書いていきます。
#!/usr/bin/env perl use strict; use warnings; use lib ('./lib'); use Acme::MetaVar; my $am = Acme::MetaVar->new(); print $am->hoge() . "\n"; # hoge
prove -l
で確認、無事動いてるようです。
さて、テスト用のファイルなのですが、Minilla
が作成する雛形にはテスト用のスクリプト置き場があります。
t/
ディレクトリです。
ここにテストスクリプトをおくことで、 prove -l
した際に一緒にテストしてくれるようになります。
せっかくなので、ここで先ほどの hoge
メソッドを呼び出すテストを書いてみましょう。
先に作成したテスト用スクリプトは削除しておきます。
$ rm test.pl
今回は hoge
メソッドを読んだときにちゃんと hoge
という文字列が返ってきたら正常稼働とします。
$ touch t/01_hoge.t $ vim t/01_hoge.t
use strict; use Test::More 0.98; use Acme::MetaVar; my $am = Acme::MetaVar->new(); ok( $am->hoge eq 'hoge' ); done_testing;
文字列の比較演算子 eq を用いて、メソッドの返り値が hoge
であるかを確認しています。
テストについてはここをよく参考にしたりしています。
さて、 prove -l
で確認します。
$ prove -l t/00_compile.t .. ok t/01_hoge.t ..... ok All tests successful. Files=2, Tests=2, 0 wallclock secs ( 0.04 usr 0.01 sys + 0.22 cusr 0.06 csys = 0.33 CPU) Result: PASS
おおっ!気持ちいい〜!
このように、テストしたい対象やテストするべきことがはっきりしているとテストも書きやすいです。
そう、意外に自分が何を求めているのか、わからないことも結構あるんですよね・・・人生と同じで・・・
ではここでまたgit add .
& git commit
しておきます
$ git add t/01_hoge.t $ git commit -m "add test hoge" [master 8c0ef38] add test hoge 1 file changed, 10 insertions(+) create mode 100644 t/01_hoge.t
メソッドを増やす!その前に
ここで、新たにメソッド fuga
を追加することになりました。使用イメージはこんな感じ。
print $am->fuga() . "\n"; # fuga;
Acme::MetaVar
にサブルーチン追加していけば良さそうですね。
しかし、この先も同じような追加改修が続くかも知れません。piyo
, foo
, bar
, buz
...
見通しの良いスクリプトにするために、メソッドごとにファイルを分けて管理することにします。
まずは、Acme::MetaVar
から hoge メソッドを分離します。
Acme/MetaVar.pm
から hoge
サブルーチンを削除して、 Acme::Hoge
モジュールを use
します。
package Acme::MetaVar; use 5.008001; use strict; use warnings; our $VERSION = "0.01"; use Acme::Hoge; # 追加 sub new { my $class = shift; my $self = bless {}, $class; return $self; } 1; __END__ (中略)
そして、新規に Acme::Hoge
モジュールを作ります。MetaVar.pm
と同じ階層。treeで見るとこんな感じ。
├── lib │ └── Acme │ ├── Hoge.pm │ └── MetaVar.pm
hoge
メソッドを他のモジュール(今回は Acme::MetaVar
)経由で利用できるよう、エクスポートしています。
package Acme::Hoge; use 5.008001; use strict; use warnings; our $VERSION = "0.01"; use Exporter 'import'; # エクスポーター our @EXPORT = qw/hoge/; # エクスポートするメソッド sub hoge { return "hoge"; }
では、これで prove -l
でテストします・・・大丈夫ですね。
ではここでまた git で保存しておきます。
$ git add . $ git commit -m "Moved the hoge method to another file" [master ea84d86] Moved the hoge method to another file 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 lib/Acme/Hoge.pm
メソッドを分離しました。
fuga メソッドを「テストファースト」で追加してみる
さて、直近の課題たる fuga
メソッドの実装です。・・・せっかくなので「テストファースト」で書いてみます。
先にテストを書くことで、スクリプトに何を求めているのか?をはっきりさせる効果があります。
では、hoge
メソッドと同じように、fuga
メソッドのテストを書いてみましょう。
と言っても、hoge
をコピーしてファイル名と中身をちょっと変えるだけです。
fuga
メソッドのテストファイルは 02_fuga.t
とします。
$ cp t/01_hoge.t t/02_fuga.t $ vim t/02_fuga.t
02_fuga.t
の中身です。
use strict; use Test::More 0.98; use Acme::MetaVar; my $am = Acme::MetaVar->new(); ok( $am->fuga eq 'fuga' ); done_testing;
では、prove -l
です。
$ prove -l t/00_compile.t .. ok t/01_hoge.t ..... ok t/02_fuga.t ..... Can't locate object method "fuga" via package "Acme::MetaVar" at t/02_fuga.t line 8. t/02_fuga.t ..... Dubious, test returned 255 (wstat 65280, 0xff00) No subtests run Test Summary Report ------------------- t/02_fuga.t (Wstat: 65280 Tests: 0 Failed: 0) Non-zero exit status: 255 Parse errors: No plan found in TAP output Files=3, Tests=2, 1 wallclock secs ( 0.04 usr 0.02 sys + 0.32 cusr 0.08 csys = 0.46 CPU) Result: FAIL
はい失敗しました。まだ Acme::Fuga
も作ってないし、 Acme::MetaVar
内で use Acme::Fuga;
してないからですね。
分かっていればテスト失敗も怖く無い・・・プッチ神父に通じる精神ですね。
では作っていきます。
package Acme::MetaVar; use 5.008001; use strict; use warnings; our $VERSION = "0.01"; use Acme::Hoge; use Acme::Fuga; sub new { my $class = shift; my $self = bless {}, $class; return $self; } 1; __END__ (中略)
package Acme::Fuga; use 5.008001; use strict; use warnings; our $VERSION = "0.01"; # Exporterを継承 use Exporter 'import'; # エクスポートする関数を記述 our @EXPORT = qw/fuga/; sub fuga { return "fuga"; }
さて、どうでしょうか?
$ prove -l t/00_compile.t .. ok t/01_hoge.t ..... ok t/02_fuga.t ..... ok All tests successful. Files=3, Tests=3, 0 wallclock secs ( 0.05 usr 0.02 sys + 0.32 cusr 0.08 csys = 0.47 CPU) Result: PASS
はい、テストに通りました。
$ git commit -m "add fuga method" [master 1218746] add fuga method 3 files changed, 25 insertions(+) create mode 100644 lib/Acme/Fuga.pm create mode 100644 t/02_fuga.t
fuga
メソッドを追加しました。
こんな感じで作っていくんだけど・・・
さも間違わずに迷うことなく出来上がったように書いてますが、もちろんそんなことないです。
たくさんのエラーに塗れておりますし、git status
で確認しながらの作業です。
今回の記事は、だいたい2年くらい前の自分向けの記事でした。
追記
use Acme::Hoge; use Acme::Fuga;
ってところ、単に hoge
, fuga
メソッドを使えるようにしているだけなのですが、これって継承では?やりたかったのって継承では?ってことで use base
しました。
さらに追記
base
はもう10年も前に非推奨ですね・・・ってことで use parent
に置き換え〜