この記事はPerl Advent Calendar 2017の8日目の記事です。
昨日は @masakyst さんでCarmelとcpmでした。cpm使っております! Carmelも今度使ってみよっと。
はい、どうも、Perlは古臭いなんて言わせないの会の会員のマコピーです。
宣伝
12月末に発売されるWEB+DB Press vol.102のPerl Hackers Hubを担当させていただきました。内容はゲーム開発の話なんですが、その過程で生まれたモジュールであるTest::MasterData::Declareについてここで紹介させていただきます。
Test::MasterData::Declareとは
Test::MasteData::Declare - metacpan
Test::MasterData::DeclareはRDBMSや表計算ソフトのような行指向データ構造(と言いつつ現在はCSVのみですが)のテストを行うモジュールです。ここでいうテストは何かというと、
- カラムの値の型と値域など、値自体のバリデーション
- 複数のテーブル(RDBMSで言うテーブルで、CSVで言う別々の構造を持ったCSVファイル)間のリレーションの整合性
などを行えるツールです。って言ってもまだ開発途上で目標のすべてを達成できているわけではありません。
提供する体験としては、
- 複雑な構造を持った行指向データの妥当性のチェック
- DBを介さずに高速にデータのテストを行える
- どの行でfailしたかをCSVファイルの行数で見ることが出来る
と言ったところです。
Test::MasterData::Declareの使い方
とりあえずcpanm
しましょう。
$ cpanm Test::MasterData::Declare
あとテストするCSVファイルを用意します。
id,name,power1,power2,power3,power4,price1,price2,price3,price4
1,"おじいちゃん",0,1,0,1,0,1,1,1
2,"椅子畑",0,1,1,1,0,1,2,1
3,"椅子採掘場",1,10,0,2,1,3,1,2
4,"椅子工場",1,24,1,2,1,10,0,3
5,"禁断の秘術†椅子†",1,25,100,3,2,20,20,2
6,"確定拠出椅子",1,30,147,13,1,22,69,17
元ネタ: isucon/isucon7-final
これをテストファイルから読み込みます。
use Test2::V0;
use Test::MasterData::Declare;
master_data {
load_csv item => "item.csv";
...
};
done_testing;
master_data
というブロック内でload_csv
でこのように読み込むと、Test::MasterData::Declare
のtable
関数からはitem
という名前でcsvを参照することが出来ます。
ここでは、このマスタデータを入れるRDBMSはMySQLで、name
カラムはutf8mb4
ではなくutf8
のカラムだと仮定してテストを書いてみます。つまり、utf8で4バイトで表現される文字(絵文字など)を検知するテストです。
4バイトのutf8文字列を引っ掛ける正規表現は
[\x{10000}-\x{10ffff}]
で表すことが出来るため、
master_data {
load_csv item => "item.csv";
table item => "name",
mismatch(qr![\x{10000}-\x{10ffff}]!);
};
と書けばテストでfailさせることが出来ます。
mismatch
はTest2::V0をuse
して使用することができる関数です。この関数はTest2::Compare::Regexを返します。Test::MasterData::Declare
はTest2::Suite
をベースに作られているため、Test2のCompareオブジェクトをそのまま使うことが出来ます。
これをprove
で実行します。
$ prove -l t/item.t
All tests successful.
Files=1, Tests=1, 0 wallclock secs ( 0.02 usr 0.01 sys + 0.09 cusr 0.01 csys = 0.13 CPU)
Result: PASS
そうですね、絵文字がname
に入っていないので成功します。
ここでマスタデータに絵文字を入れてみましょう。
id,name,power1,power2,power3,power4,price1,price2,price3,price4
1,"おじいちゃん",0,1,0,1,0,1,1,1
2,"🌾椅子畑🌾",0,1,1,1,0,1,2,1
3,"椅子採掘場",1,10,0,2,1,3,1,2
4,"椅子工場🏭",1,24,1,2,1,10,0,3
5,"禁断の秘術†椅子†",1,25,100,3,2,20,20,2
6,"確定拠出椅子",1,30,147,13,1,22,69,17
$ prove -l t/item.t
t/item.t .. 1/?
# Failed test at t/item.t line 11.
# +-----------------------+-----------+----+-----------------------+-----+
# | PATH | GOT | OP | CHECK | LNs |
# +-----------------------+-----------+----+-----------------------+-----+
# | {item.csv#id=2}{name} | 🌾椅子畑🌾 | !~ | (?^u:[\x{10000}-\x{10 | 3 |
# | | | | ffff}]) | |
# | | | | | |
# | {item.csv#id=4}{name} | 椅子工場🏭 | !~ | (?^u:[\x{10000}-\x{10 | 5 |
# | | | | ffff}]) | |
# +-----------------------+-----------+----+-----------------------+-----+
# Seeded srand with seed '20171208' from local date.
t/item.t .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/1 subtests
Test Summary Report
-------------------
t/item.t (Wstat: 256 Tests: 1 Failed: 1)
Failed test: 1
Non-zero exit status: 1
Files=1, Tests=1, 0 wallclock secs ( 0.01 usr 0.01 sys + 0.12 cusr 0.03 csys = 0.17 CPU)
Result: FAIL
こんな感じで表がべろべろっとでてきます。これはTest2::Compare::Deltaの機能です。Test::MasterData::Declare
では工夫をして、PATH
にファイルとIDとカラム、LNs
にCSVファイルでの行番号を表示しています。ここめっちゃ頑張りました。便利でしょ????
他にも色々機能はあるんですが、まだまだバギーなところがあるので、紹介はこの程度にしておきます。
Test2について
Test2は、従来はTest::Builderで行っていたテストの実行や集計などを全面的に書き直したモジュールです。
この説明については、
が詳しいです。簡単に言うと前と比べて、テスト自体の拡張がしやすくなったのと、テストモジュールのテストも書きやすくなったということです。Test::MasterData::Declare
は前者の特徴を利用しています。
Test2はTest::SimpleとTest2::Suiteのそれぞれに実装がされています。Test::Simpleのほうではテストの実行と集計を司り、Test2::Suiteは拡張しやすくなったTest2を用いて豊富なカスタムマッチャーの実装がされています。従来のTest::Deep::Matcher
の置き換えになるわけですね。
一般的にはTest2::V0
をuse
すれば色々使えて便利です。
Test2::Compare::**
Test2::V0
をuse
した状態でlike $got, $check
とした場合、$check
が、Test2::Compare::**
になります。Test2::Compare::**が持っている
runというメソッドに
$got`を放り込むことで検査が行われます。
この変換ロジックはこの辺です。
Test::MasterData::Declare
では、$got
に当たる部分を、CSVの各行をHashにしたものを、Test::MasterData::Declare::Row
という独自クラスに変換しています。このクラスにはCSVでいう行番号やカラムのinflateの際に使うjsonメソッドなどが実装されています。
これをそのままTest2::V0
のhash
で、
like $rows, hash {
field "name" => ...;
};
とすることは出来ないので、この$check
の方にも、独自のCompareクラスを使っています。それがTest::MasterData::Declare::RowHash
です。Test::MasterData::Declare
はTest2::V0#hash
で作られるTest2::Compare::Hash
を継承していますが、検査をする時に$got
からrow
というメソッドで生ハッシュを取り出して本来の検査メソッドに渡しています。で、結果が返ってくると、これはTest2::Compare::Delta
というオブジェクトになっていて、これに結果を表示するための差分が入っているのですが、ここで行番号とPATHを$got
をもとに書き換えています。ここまでしてやっと表示があそこまで便利になるのです。これを編み出すのにめっちゃ苦労しました。
Test2調べてる
— トーカナイザの守護霊 (@mackee_w) 2017年9月29日
Test2わかってきた
— トーカナイザの守護霊 (@mackee_w) 2017年11月14日
今日だけでTest2わかる/わからないを10回ぐらいいったりきたりしてる
— トーカナイザの守護霊 (@mackee_w) 2017年11月14日
噛めば噛むほど困難が立ち向かってくるTest2
— トーカナイザの守護霊 (@mackee_w) 2017年11月17日
あとTest2::Suite
で使われているTest2::Util::HashBase
っていうクラスビルダーがわりかしキモいです。なんなんだあのプロパティアクセスの仕方は……。。。
まとめ
- Test::MasterData::Declareで宣言的にマスタデータのテストを書いて、テストの民主化をするぞ!!!!
- Test2を使えば便利テストモジュールが(比較的)簡単に書けます!
というわけで、みんなもテストモジュール書こうぜ。明日は @ytnobody さんで「コアモジュールに寄せてみよう」です。なるほど、ワクワクしそうなタイトルです。