― Web Technology and Life ―

型一致ベースのテストに使えるPerlモジュール ~Test::Deep::Matcher,MouseX::Types~

2012-08-03
『PerlでTDD(テスト駆動開発)するなら覚えておきたいCPANモジュール群 』って記事書いたら、ありがたいことにikasam_aさんに「Tes::Deep::Matcherを書いたよ」ってご紹介頂きましたので、続けざまに型一致ベースのPerlのテストについていろいろと思うところを整理しておきたいと思います。

【宣伝】Yapc::Asia2012のトークに応募しています

この記事アップしようとして、先の記事みたら、私のブログからしたらたくさんはてぶが付いているじゃありませんか!!そして、そのわり・・・。だったので、最初に紹介します。

Perlの最大のイベントYapc::Asiaが今年も開催されますが、今年はトークに応募してみました。バッチ処理とかジョブキューシステムとのQudoとかについて普段やっていることをまとめて発表する予定です。ご興味ありましたら、是非『不安定な環境の中でのバッチ処理~JobQueueシステムQudoを使った事例~』に「いいね!」とかくださるとうれしいですー!

【要件】なぜ型一致のテストを書きたいか?

では仕切り直しまして。私が、経験した限りだとざっくり思いつく限り2ケースほどあります。皆様は数多くの開発案件に携わっておりさらに多くのケースに出くわしていると思いますが、私の拙い経験上からその必要性について口上たれたいと思います。

WebAPIなど外部のデータ形式を引っ張ってくるケース

APIを使ったりすると、JSONやらXMLで結果が返ってきたりして、Perlのデータ構造にしても何かと複雑になります。Perlの場合は、JSONならJSONモジュールにまかせておけばパースに失敗するとかいう想定はあまり要らないと思うのですが、XMLで返ってくるとXML::LibXMLなど使って多少テクニカルな操作が必要になってきて、まぁ動作確認しておきたいなーなんてケースになるわけですね。

それで、地道にTest::Moreのis_deeplyで比較しようとすると、例えば以下のようなテストコードになっちゃったりすると思うんです。

use strict;
use warnings;
use Test::More;
use Path::Class;
use MyParser;

my $xml = file('./t/MyParser/sample.xml')->slurp(iomode => '<:encoding(UTF-8)');

my $data = MyParser->parse($xml);

is_deeply $data, +{
    header_section => {
        total_results => 2314422,
        response_time => '2012-05-21 12:32:12',
    },
    results => [
        {
            name       => 'hirobanex',
            ange       => 27,
            sex        => 'male',
            self_intro => 'いつも楽しくプログラミングしていますっ!たまに八王子行ったりしますっ!でも、最近はプログラミングがあまりできておらず、
もやっとしていますっ!焼肉も好きですけど、魚も大好きで、
寿司とか天ぷらとか大大好きですっ!今度誰かおごってくださいw',
        },
        {
            name       => 'hogehoge',
            ange       => 32,
            sex        => 'female',
            self_intro => 'hogehoge,mogemoge,doradra,hogehoge,mogemoge,doradra,hogehoge,mogemoge,doradra,hogehoge,mogemoge,doradra,hogehoge,mogemoge,
doradra,hogehoge,mogemoge,doradra,hogehoge,
mogemoge,doradra,hogehoge,mogemoge,doradra,hogehoge,mogemoge,doradra,',
        },
        .
        .
        .
        (上記のhashrefが10~20くらい続く)
        .
        .
        .
    ],
};

done_testing;

予め用意されているAPIのサンプルだったり、とりあえずAPI叩いてみてとったサンプルだったりを、テストデータとして使って丁寧にやるとこうなると思います。これは、自己紹介みたいな短文もうざいし、膨大な量の文字列をいちいちテストコードに入れるのはとてもしんどいし、見通しが悪くなるし、兎にも角にも、テストコードの保守性が損なわれてくるわけです。うまいこととった$dataをクルクル回しながら、「cmp_ok length($child_data),'>',0」みたいなことやってもいいんですけど、それでもテストコードの仕様性というかそういう保守性が損なわれてくると思うんですね。

要するに、「根本的に、データ構造(hashrefのkeyの入れ子状態)がシンプルにテストできてさえすればいいのに!」という気が私はしてくるのです。

ランダムで何かを生成するケース

rand関数とか、あるいは、Sub::RateData::WeightedRoundRobinなどを使って、ランダムに何か生成してリターンされるコードってガチャとか流行っているみたいなんで、そこそこあると思うんですが、これも普通にTest::Moreを使うとわりと味気ない気がします。

use strict;
use warnings;
use Test::More;
use MyGenerator;

my $data = MyGenerator->gacha();

#とりあえず、シンプルに
ok $data;

#せめて、ハッシュかどうかだけでも
is ref($data),'HASH';

#中に何が入っていればよい
ok $data->{card_id};
ok $data->{card_name};
ok $data->{level};

done_testing;

まぁ、いろいろと人によってどこまでテストするのかの判断が違ったり、ここレイヤーのコードはここまでテストできていればオッケーっていうところもあるんで、一概にはいえないんですが、私は細かい性格なんで、いろいろ気になってしまうんですね。

  • 一番目の「ok」のケースは、リターンがちゃんとできていなくて最後の評価値がBoolでtrueで返ってきているんじゃないか?
  • 二番目に「ref」のケースは、確かにhashrefが返ってきているかもだけど、「+{}」だったりするんじゃないのか?
  • 三番目のケースでさえ、$data->{card_id}の中身と$data->{card_name}が入れ替わってしまっているんじゃないか?

という具合にです。

ただ、ランダムでなんか生成しちゃっているんで、実際の中身が定義できないんですよねー。だから、「せめて、valueの中身の"雰囲気"だけでもチェックできればなー」と思ってしまうのです。

Test::Deep,Test::Deep::Matcherで型一致ベースのテスト

Test::Deep知ったときも「うっひょ!」と思ったんですが、Test::Deep::Matcherもさらにさらに「うっひょ!うっひょ!」と思いました。

「WebAPIなど外部のデータ形式を引っ張ってくるケース」をTest::Deep(::Matcher)に置き換えてみる

さて、早速、Test::DeepとTest::Deep::Matcherを使って、「WebAPIなど外部のデータ形式を引っ張ってくるケース」のテストコードを書きなおしてみましょう。

use strict;
use warnings;
use Test::More;
use Test::Deep;
use Test::Deep::Matcher;

my $xml = file('./t/MyParser/sample.xml')->slurp(iomode => '<:encoding(UTF-8)');

my $data = MyParser->parse($xml);

#cmp_deeplyとarray_eachはTest::Deepの関数
cmp_deeply $data, +{
    header_section => {
        total_results => is_number,
        response_time => is_string,
    },
    results => array_each({
        name       => is_string,
        ange       => is_number,
        sex        => is_string,
        self_intro => is_string,
    }),
};

#or
cmp_deeply $data, +{
    header_section => is_hash_ref,
    results        => is_array_ref,
};

done_testing;

すばらしいですねっ!コードが、数十行と短縮されますし、すっきりとテストしている内容がわかりやすいですねっ!

Test::Deep::Matcherは、リファレンスかどうかについての関数(is_scalar_ref,is_array_ref,is_hash_ref,is_code_ref,is_glob_ref)と、基本的な型かどうかのについての関数(is_value,is_string,is_number,is_integer)についてエクスポートしてくれて、上記のようにハッシュリファレンスのキーのバリューにつけてテストすることができます。Test::Deep単体だとそういうことはできなくて、先の『PerlでTDD(テスト駆動開発)するなら覚えておきたいCPANモジュール群』でも書きましたし口述しますが、複雑なデータ構造のvalueの型までチェックしようとしたときにMouseX::Types::Mouseをかませて使うひつようがあり、MouseX::Types系の学習コストが必要になってきます。

入れ子でも使えるし、なかなかシンプルで気持ちよいですねっ!今後、どしどし使わせて頂きますっ!

MouseX::Types::Mouseの思い出

Test::Deep::Matcherの紹介も終わったし、事後談ですが、ちょっと私の話をつらつらとします。「型的なテストしたいけどなんかいいのないのかなー」と思っていたときに、Smart::ArgsとかData::Validatorとかを知って、MooseとかMouseとかで使われている型とかさらにそれらを使って独自の型を作れちゃうよ!っていう、Mouse::Util::TypeConstraintsを知ったわけです。

「ふーん、なんかいいなーテストにも使えないかな―」とか思っていると、Test::Deepなるモジュールがあることを知り、そして汎用的な型をいろんなところで使えるM[ou]seX::Typesを『Mouse::Util::TypeConstraints等を使って新しい型を定義するときのベストプラクティス』という記事で知りました。

そんなこんなで、ちょっと興奮気味にMouseX::TypesとかMouseX::Types::Mouseを使って型ベースのテストをしていたわけです。

で、せっかくなので、これらはとてもいいモジュールですし、なかなか使い勝手細かくていつもPODを見返すのでw、いろいろと思いだし折に忘れないようにメモすると同時に、ツッコミどころがないか検証する意味でも、シェアさせて頂きます。

MouseX::Types::Mouseで型を定義してテストで使う

MouseX::Types::MouseはMouse::Util::TypeConstraintsで使える以下などの型をエクスポートして使えます。

概要
Bool 真偽値か
Str 文字列か
Int 数字か
ScalarRef スカラーリファレンスか
ArrayRef 配列リファレンスか
HashRef ハッシュリファレンスか
CodeRef コードリファレンスか
RegexpRef 正規表現のリファレンスか
Object オブジェクトか

ちょっとわかるづらいかもしれないですが、とりあえず、POD見るか以下のサンプル見てください。テスト的にうれしいのが、上記の型に「is_」という接頭語をつけると、その型かどうか真偽判定をしてくれるという機能も同時についてくるのです。それで、こんな感じに使えます。

use Test::More;
use Test::Deep;
use MouseX::Types::Mouse qw/Int RegexpRef/; #Int型とRegexpRef型をエクスポート

my $hoge = 1234;

ok is_Int($hoge);

my $regex = qr/^hirobanex.net.+/;

ok is_RegexpRef($regex);

done_testing;

「おぉ!」と、なんか未来が見えてきました。そう思いませんか?

MouseX::Types、独自の型も使ったテストを書く

MouseX::TypesはMouseX::Types::Mouseで提供されるM[ou]seのデフォルトの型などを駆使しつつ独自の型を定義できるモジュールです。詳しくは、MouseX::TypesのPODをご覧いただきたいのですが、とりあえず、サンプルです。

まず、こんな感じに独自の型を定義します。

package MyApp::Model::Types;
use strict;
use warnings;
use utf8;
use MouseX::Types -declare => [qw/Url SEX ResultSets/]; #独自に定義した型をエクスポート定義する
use MouseX::Types::Mouse qw/Int Str/; # Mouseのデフォルトで提供される型をこのコードにエクスポート
use Test::Deep;

subtype Url,
    as Str,
    where {$_ =~ /^s?https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#]+$/; };

enum SEX,
    qw(
        male
        female
    );

subtype ResultSets,
    as ArrayRef,
    where {
          eq_deeply($_,
              array_each({
                  id          => \is_Int,
                  strings     => code(sub{$_[0] && is_Str($_[0])}),
                  include_url => \is_Url,
              })
          )
    };

1;

これをテストコードで使います。

use Test::More;
use Test::Deep;
use MyApp::Model::Types qw/Url SEX ResultSets/;
use MouseX::Types::Mouse qw/Int Str/;
use Test::Deep;

my $data = +{
    name        => 'hirobanex',
    profile_url => 'http://hogehoge.net/hirobanex',
    mentions    => +[
        +{
              id          => 123,
              strings     => 'hogehoge',
              include_url => 'http://aaa.com/',
        },
        +{
              id          => 234,
              strings     => 'gerogero',
              include_url => 'http://yapcasia.org/2012/talk/show/2c531ede-c1ac-11e1-860d-28556aeab6a4',
        },
    ],
};

cmp_deelpy $data,{
    name        => \is_Str,
    profile_url => \is_Url,
    mentions    => \is_ResultSets,
};

done_testing;

型の仕様をゴリゴリに固めるなら、MouseX::Types使って独自の型を作るのはとてもコードから仕様が読み取りやすく良いと思います。もちろん、MouseX::Typesで作った型のテストもしっかりとやらないといけませんがw

とりあえず、私は型大好きな人間なんで、最近は、MouseX::Typesで定義した型を、テストでは「is_」を使って、モデルではSmart::ArgsとかData::Validatorで使って、Controller(Web)では、FormValidator::Lite::Constraint::Mooseで使って、ウハウハしていたわけです。

MouseX::Typesって「to_」接頭詞使えない?

ちなみに、 MooseX::Typesのほうでは、「to_」という接頭語もエクスポートしてくれて、coercion(別の型で入ってきても一定の指定したルールに基づく型変換)をするのがあるんですが、MouseX::Typesのほうはないっぽいです。間違っていたら、すみません。。。なんか理由があるのかよくわからんですが、個人的にはMouseX::Typesにも「to_」があると、テストに便利だなーと思うわけですが、ここ最近は新規コードも書かず、パッチを送るためにコードを読む的なモチベがなく・・・。すみません。。。

最後に ~テストダブルとか~

ikasam_aさんの以下の記事とスライドは、抽象的ですがとても参考になるので、都度都度見返して咀嚼させて頂いております。大変感謝です。

一概に動作確認と言っても、どういうテストが適切かを考えると大変奥が深く難しいものです。そして、直観的にやりたいテストを書くのも基本のTest::Moreなどでは難しいケースがあり、型テストならTest::Deep(::Matcher)、HTTP含みのテストならTest::Fake::HTTPD,Test::Mock::LWP::Conditional、と知っていた方が確実に実装が早くなるケースがほとんどだと思います。

抽象的な定義から始まってその実装をCPANモジュールにあげているCPANオーサーの方々はホントにすごいし、ありがたい限りですねっ!

Perl update_at : 2012-08-03T16:10:21
hirobanex.netの更新情報の取得
 RSSリーダーで購読する   
blog comments powered by Disqus