Test::Arrow というテストモジュールを書いている
こんにちは。4年に一度の2月29日です。日付周りの処理でバグが多発する時期です。みなさん、テストは書いていますか。テストですよ、テスト。テストはとても大事です。あの日あの時あのバグに、なぜテストはなかったのでしょうか。ま、そんなことを深く考えるより、だまってテストを書きましょう。テストの書けるコードを書きましょう。とはいえ、テストコードにも無駄は発生するのでただ書けば言い訳ではありませんが。
さて、ここ近年のPerl5界隈では Test2 が Test::More の代替として開発が進んでいますが、Perlのテスト用モジュールは昔からとても充実しています。Test::Exception , Test::Warnings , Test::Deep といったモジュールはPerl Mongersなら馴染み深いと思います。Test2 においては Plugin として同じような機能が提供されているようです。他にもテストモジュールは山ほど CPAN にあります。例えば、RSpec/Jasmine、BDD、Given-When-Then などのスタイルを踏襲した Test::Kantan や Test::Ika といったモジュールも便利ですね。
すでに大充実しているPerlのテストモジュールなのですが、どれも DSL なんですよね。そりゃあそうね、Perlですから。真骨頂。ただ、数年前まで自分がPerlをお仕事でも毎日書いていたころは、DSL がとても楽で便利でカッコイイと思っていたのですが、PerlがWeb向けの言語として多数派ではなくなり、さらにmicroservices 全盛の昨今にあっては、サービス間で言語は違うがメンテナンスする人は同じというような状況も多いのではないかと思います。そういう場合、Perlアプリまたはテストにおいても記述のあり方は他言語と共通性が高い方が良いのではないか、と思うようになりました(DSLの方が共通性が高いわと言う突っ込みはちょっとまって)。
というのも DSL は記述が簡単なかわりに言語的な習熟がむしろ必要で、Perlの場合コードブロックとハッシュがわかりにくいとか、スペースで区切るのかカンマがいるのかがまたこれわかりにくくてコンフュージングなのです。頭痛が痛い。いや、自分もそんなにネガティブに思ったことはあまりなかったんですけど、ここ数年はしばらく Perl を離れて他の言語触ったあとに戻ってくる機会があって、あ、ちょっとこれわかりにくいかも、くらいには思っていたのです。そもそも got と expected の順番わからん!とかね。それは言語関係ない話ですけど。
そういうわけで、じゃあ DSL だけど気にせず括弧を全部付けて書けばある程度はわかりやすくなるんじゃない?、とか、ほとんどのテストモジュールではコードリファレンスを DSL 的に渡すのではなく、sub 付きで渡すこともできるから、そういう風にすればいいのかなと思ったりしたのですが、なんか、しっくりこなかったのですよね。そんなんじゃあ今度は DSL の良さが丸つぶれな気がして。
それでいろいろ考えていたのですが、自分のもやもやしたこの疑問への回答は Object Oriented に寄せきった記述なのではないかとある日思いついたのです。スクリプティング言語だけど。なぜなら、Object Oriented は各言語だいたい同じだし(細かいこと言うとそうでもないし、PerlのOOがそもそもという突っ込みはここではしないでね)、んまあアプリケーション側はたいてい Object Oriented な記述をしていますよね? だったらテストも Object Oriented に書けたら実は楽なんじゃないか、と思いついてしまったのです。
誰もがそう思うかはさて置き、とにかく書いてみようということで書きました。 Test::Arrow です。以下のような感じ。
use Test::Arrow;
my $arr = Test::Arrow->new;
$arr->got(1)->ok;
$arr->expect(uc 'foo')->to_be('FOO');
$arr->expected('FOO')
->got(uc 'foo')
->is;
$arr->expected(qr/^ab/)
->got('abc')
->like;
$arr->warnings(sub { warn 'Bar' })->catch(qr/^Ba/);
$arr->throw(sub { die 'Baz' })->catch(qr/^Ba/);
矢印だらけですねw
とりいそぎ、このモジュール自身のテストを全部カバーできる程度には機能を揃えたけど、まだ粗いかもしれないので是非使ってみて issue あげて欲しいです。ここから残りは Test::Deep みたいな複雑なデータ構造をテストする機能と NoWarnings 的なの入れたら自分のやりたいことはひと通りそろう感じ(Test::Deepは相当ヘビーなのでやる気が出るかわからない)。ちなみに突っ込まれる前に書いておきますが、subtest 的なことはいったん捨ててます。describe?あとまわしあとまわし。あと、車輪の再発明なのかもしれない(ないと思うけど)。
とはいえ まあまあメソッド揃っているので詳細は POD を参照願います。けっこうかゆい所に手を届かせたつもりです。個人的には explain と x という2つのメソッドが気に入っていて、まず explain は Test::More にある引数をダンプするおなじみの explain関数と diag関数をそのまま結びつけてて、 diag explain($foo)
と書かずに explain だけでダンプしたものを diag に渡したのと同じ挙動をします。そしてそれを引数なしで呼ぶと、name/got/expected をまとめて自動的にダンプしてくれます。テストする値を、オブジェクトに持っているのでコンテクストの如くまとめて吐くのが簡単なのですね。
引数ありの explain は引数だけをダンプ
$arr->name('expalin test')->expected('BAR')->got(uc 'bar')->explain({ foo => 123 })->is;
# {
# 'foo' => 123,
# }
引数なしなら以下のようにセットした値がでる。
$arr->name('expalin test')->expected('BAR')->got(uc 'bar')->explain->is;
# {
# 'expected' => 'BAR',
# 'got' => 'BAR',
# 'name' => 'expalin test'
# }
便利。
さらに、x というメソッドは explain をもっとデバッグ向け(テストだけどデバッグ!)にしていて、引数と name/got/expected 値、全部を吐きます。
$arr->name('x test')->expected('BAR')->got(uc 'bar')->x({ foo => 123 })->is;
# {
# 'foo' => 123
# }
# {
# 'expected' => 'BAR',
# 'got' => 'BAR',
# 'name' => 'x test'
# }
これ、x があれば explain 必要なくて、いつも x メソッドだけでいいんじゃないか? と思うかもしれませんが、一応、違いがあって、explain は書いてあるもの全部いつも出力されるのに対し、x メソッドの出力は、コンストラクタで一括制御できるようになっています。つまり、x メソッドはデバッグ用途でばんばん書いておいて、まとめてオンオフができるのです。
以下のようにオフすることができる。デフォルトはオン。
my $arr = Test::Arrow->new( no_x => 1 ); # x メソッドの出力 off
# x は何も出力しない
$arr->name('x test')->expected('BAR')->got(uc 'bar')->x({ foo => 123 })->is;
超便利じゃないですか?
ま、まあ、いつもきれいにすいすいテストが書ける人にはいらない機能かもしれないですけど、そもそもテストですし。でもこれ自分にとってはめっちゃ捗ります。というわけで、なんかダンプメソッドの紹介になってしまったけど、普通のテストメソッドもそろってますので、よろしくお願いします。
ではでは。ごきげんよ~。