Masteries

技術的なことや仕事に関することを書いていきます.

PerlでスナップショットテストをするTest::Snapshotのご紹介

このエントリは, 「Perl Advent Calendar 2020」の9日目の記事です.

qiita.com

昨日のエントリは, id:xtetsuji さんの「xargs や find と合わせて使う・代わりに使う Perl」でした.

qiita.com


実は最近異動をしていた id:papix です. 異動後もPerlをモリモリ書いている日々ですが, 移動先のチームのプロダクトで同僚の id:mizdra が導入していた Test::Snapshot が便利だったので紹介します.

metacpan.org

Test::Snapshot

Test::Snapshotは, その名の通り「スナップショットテスト」を提供するモジュールです. スナップショットテストとは, 予め「スナップショット」と呼ばれる期待値を生成しておき, テストを実行する際には実行結果とスナップショットを比較してテストをする手法です.

使い方

まず初めに, Test::Snapshotを利用したテストの実例を見てみましょう:

use Test2::V0;
use Test::Snapshot;

sub method {
    return {
        a => 1,
        b => 2,
        c => [3, 4, 5],
    };
};

is_deeply_snapshot method(), 'hashref';

done_testing;

これは, method() の返り値をスナップショットテストで検証するコードになります. なおここでは, 単純にするためにテストスクリプトに直接テスト対象となる実装(method)を書いています.

このコードを, snapshot.t という名前で保存し, prove で実行してみます.

$ prove -v snapshot.t
snapshot.t ..
# Seeded srand with seed '20201209' from local date.
# No snapshot filename '/path/to/dir/snapshots/snapshot_t/hashref' found
not ok 1 - hashref

#   Failed test 'hashref'
#   at snapshot.t line 12.
# @@ -1 +1,9 @@
# -undef
# +{
# +  'a' => 1,
# +  'b' => 2,
# +  'c' => [
# +    3,
# +    4,
# +    5
# +  ]
# +}
1..1
# Looks like you failed 1 test of 1.
Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/1 subtests

Test Summary Report
-------------------
snapshot.t (Wstat: 256 Tests: 1 Failed: 1)
  Failed test:  1
  Non-zero exit status: 1
Files=1, Tests=1,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.07 cusr  0.02 csys =  0.11 CPU)
Result: FAIL

まだスナップショットを生成していないため, 当然ですがテストは失敗しますね.

# -undef
# +{
# +  'a' => 1,
# +  'b' => 2,
# +  'c' => [
# +    3,
# +    4,
# +    5
# +  ]
# +}

Test::Snapshotの便利なところとして, このようにスナップショットと実行結果の差をいい感じにダンプしてくれる機能があります. ここでは, スナップショットがまだ存在しないため undef となっていることがわかります.

スナップショットの生成

さて, 続いてスナップショットを生成してみましょう. スナップショットを生成するには, TEST_SNAPSHOT_UPDATE という環境変数に真値をセットした上でテストを実行すればよいです.

$ TEST_SNAPSHOT_UPDATE=1 prove -v snapshot.t
snapshot.t ..
# Seeded srand with seed '20201209' from local date.
# No snapshot filename '/path/to/dir/snapshots/snapshot_t/hashref' found
not ok 1 - hashref

#   Failed test 'hashref'
#   at snapshot.t line 12.
# @@ -1 +1,12 @@
# -undef
# +{
# +  'a' => 1,
# +  'b' => 2,
# +  'c' => [
# +    3,
# +    4,
# +    5,
# +    {
# +      'd' => 10
# +    }
# +  ]
# +}
1..1
# Looks like you failed 1 test of 1.
Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/1 subtests

Test Summary Report
-------------------
snapshot.t (Wstat: 256 Tests: 1 Failed: 1)
  Failed test:  1
  Non-zero exit status: 1
Files=1, Tests=1,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.08 cusr  0.02 csys =  0.13 CPU)
Result: FAIL

実行すると, テストは失敗しますが, snapshot.t があるディレクトリに snapshots というディレクトリが設置されます.

$ tree
.
├── snapshot.t
└── snapshots
    └── snapshot_t
        └── hashref

2 directories, 2 files

tree するとこんな感じ. shapshots ディレクトリの中に, テストファイルに基づいたディレクトリ(テストがsnapshot.tなので, snapshot_t)が設置され, 更にその中にスナップショットの実態であるhashref というファイルが設置されます. ちなみにこのファイル名は, is_deeply_snapshot の第2引数(description)から生成されます.

注意点として, ファイル名はdescriptionに含まれる文字から, aからzまでのアルファベット(大文字小文字問わず)と0から9までの数字, そして-以外を全てまとめて_に置換したものになります.

そのため, 次のようなテストを書いてしまうと, method()other_method()のスナップショットがどちらも_というファイルに書き込まれてしまいます(!?).

is_deeply_snapshot method(), 'ひとつめのテストです';
is_deeply_snapshot other_method(), 'ふたつめのテストです';

スナップショット生成後のテスト

さて, スナップショットが生成できたので, 改めてテストを実行してみましょう.

$ prove -v snapshot.t
snapshot.t ..
# Seeded srand with seed '20201209' from local date.
ok 1 - hashref
1..1
ok
All tests successful.
Files=1, Tests=1,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.08 cusr  0.02 csys =  0.12 CPU)
Result: PASS

method()の実行結果とスナップショットは等しいので, 当然テストは成功します.

ここで改めて, methodの実装を変更してテストを実行してみましょう.

use Test2::V0;
use Test::Snapshot;

sub method {
    return {
        a => 1,
        b => 2,
        c => [3, 4, 5, { d => 10 }],
    };
};

is_deeply_snapshot method(), 'hashref';

done_testing;

意図したようにテストは失敗し, その差分を表示してくれます.

$ prove -v snapshot.t
snapshot.t ..
# Seeded srand with seed '20201209' from local date.
not ok 1 - hashref

#   Failed test 'hashref'
#   at snapshot.t line 12.
# @@ -4,6 +4,9 @@
#    'c' => [
#      3,
#      4,
# -    5
# +    5,
# +    {
# +      'd' => 10
# +    }
#    ]
#  }
1..1
# Looks like you failed 1 test of 1.
Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/1 subtests

Test Summary Report
-------------------
snapshot.t (Wstat: 256 Tests: 1 Failed: 1)
  Failed test:  1
  Non-zero exit status: 1
Files=1, Tests=1,  1 wallclock secs ( 0.02 usr  0.00 sys +  0.08 cusr  0.02 csys =  0.12 CPU)
Result: FAIL

まとめ

さくっとではありますが, Perlでスナップショットテストを実現する, Test::Snapshotを紹介しました. スナップショットを配置するディレクトリやファイル名に若干クセがある(変更はできなさそうでした)ものの, シンプルで便利なモジュールでした. 依存モジュールがかなり少なく, 基本的な(よく使う)モジュールが多いところも嬉しいです.

明日の担当は, @Taroupho さんです. よろしくおねがいします.

間接オブジェクト記法とPerl 7 (追記あり)

コードレビューする時に軽く調べたので, 備忘録として軽くまとめておきます.

2020年11月29日追記: 当初はPerl 7で間接オブジェクト記法は非推奨になる予定でしたが, 状況が変わって非推奨にしない方針となったようです.

github.com

@argrath さん情報ありがとうございました!


間接オブジェクト記法とは, こういう記法です:

my $obj = new Object;

これは, 以下のコードと同じです:

my $obj = Object->new();

「初めてみた!」, 「使ったことない!」という方もいるかもしれませんが, 実は標準エラー出力をする際の,

print STDERR 'foobar';

も間接オブジェクト記法で, 以下のコードと同じだったりします:

STDERR->print('foobar');

さて, この間接オブジェクト記法ですが, 下記エントリによるとPerl 7においては非推奨になりそうです.

www.perl.com

Perl 5.32.0以降では, no feature qw(indirect);とすることで間接オブジェクト記法の利用を制限することができ, これはPerl7においてデフォルトで有効になるようです.

Perl 7のリリース後も, Perl 5は引き続きメンテナンスされ続けます. そのため, 直ちに間接オブジェクト記法を撤廃する必要はなさそうですが, とはいえ将来的にPerl 7への以降を見据えているのであれば, 早い段階から間接オブジェクト記法を使わないようにしておく必要がありそうです.

※ 以下 2020年11月29日追記

...が, 後にPerl 7において間接オブジェクト記法の非推奨化は見送る, という方針になったようです.

github.com

理由として,

Initial testing shows a high amount of breakage on CPAN.

と書かれており, 要するに実際に非推奨にして試してみたところ, たくさんのCPANモジュールが動作しなくなったため, 断念した... ようです.

何れにせよ, Perl 7において非推奨にしたい! という声が挙がった程度には微妙な記法であることには間違いと言えるのではないでしょうか. なので, (print STDERR ...のような例を除いて)今後間接オブジェクト記法を利用するケースはないと思ってよい/既に使っている場合はなるべく使わないように書き換えていった方が良い... という温度感で良いのではないか, と思っています.

あわせてよみたい (参考文献)

kfly8.hatenablog.com

余談

Perl 5.32.0で no feature qw(indirect); とすると,

my $obj = new Object;

というコードは次のようなエラーになります:

Bareword found where operator expected at indirect.pl line x, near "new Object"
        (Do you need to predeclare new?)

一方で, 以下のコードは no feature qw(indirect); とされていても問題なく動作します:

print STDERR 'foobar';

恐らくですが, こういった形での間接オブジェクト記法の利用はかなり頻繁に使われているので, 一律で利用禁止にする訳ではなく, 例外(?)を設けているのではないかと思います.

ISUCON10の予選に参加しました(そして無事予選敗退しました)

「ISUCON参加したことないんで参加してみたいんすよね〜」という友人と一緒に, 「イスイスユカイ」で出場しました. Go実装で結果は1695点, 無事に予選落ち. 例年, ISUCONに出る時はだいたい昼過ぎくらいに何もわからなくなって, 終盤は「もうだめぽ...」となり, 何も出来ずにお通夜状態になっていることが多かったのですが, 今年はなんとか最後まで戦意喪失せずに走り切ることができました(が, 予選は敗退しました).

最後まで走りきれたのもそうですが, Goで意外と(予想以上に)スルスルと読めた/実装出来たことも良かったですね. まあ, もっと複雑なアプリケーションになってくると苦戦するのでしょうが, 今回のISUCON10の予選くらいの規模感であれば普通にやっていける, と知れたのはほんのちょっと自信になりました.

やったこと

DB分割, schema変更, 後はnginxの設定書き換えたりとかをやりました. うち, 自分が手を動かしたのが以下2つ.

DB分割

「なんかこれ, estate(物件)とchair(椅子)でJOINしているクエリなさそうじゃん?」と気づいたので, 適当にコードを書き換えて, ホストのうち1台をproxy/appに, 1台を物件用/1台を椅子用のDBサーバにする, という構成に変更しました. 2台のサーバをMySQL用にするのはチームメンバーにおまかせ. やってみたら思ったよりスコアが上がって, みんなで「ウケるw」という感じになっていました.

schema変更

なぞって検索する機能で, 緯度経度を使っていたので, そのあたりの処理をGeometry型のカラムを追加してMySQLでいい感じにできないか? ということで, fixtureの変更とGoの実装をやりました.

あとは, ある物件について椅子が入るか? というのを判定する処理がだいぶ複雑だったので, door_long, door_shortカラムを追加して, イッパツで引けるようにしたりしました. 椅子のwidth/depthについて, 短い方が物件のdoor_short/長い方が物件のdoor_longより小さかったら入る, という感じでスマートに実装できます.

fixtureの修正は, 思考停止しておもむろにPerlのスクリプトを書き始めて(Go実装でやっているのに!!!)ガッとやったのですが, 感想戦で「初期データを突っ込んで, MySQLにカラム追加して, よしなにデータ書き込んで, dumpした」という話を聞いて「それでよかったわ...」と思ったりしました.

感想

最初にも書きましたが, 今回は最後の1時間前まで予選通過ギリギリラインに踏みとどまれたのが良かったですね(しかし, そこからのラストスパートが全然出来なかったので, そこは完全に実力不足). 今回は事前に(1回だけど)過去問で練習したので, やっぱり場数踏んでおくの大事だなと思ったので, もし来年もあるのならもうちっとちゃんと練習してから望みたいですね... 特にMySQLやnginxなど, ミドルウェアのチューニングが弱すぎるので, 感覚つかんでおけると良さそうと思いました. あとは, 今回はISUCON初メンバーと一緒にやった, という所で, 一応(毎回予選敗退ではあるものの)経験者である自分がいい感じにリードする必要がある, と思うのですがそういった所あまり配慮出来なかったのも反省点です... 申し訳ない.

あと, ポータル周りは毎回進化していて凄い! と思うのですが, 今回は特に, めちゃくちゃいい感じで大変感動しました. 質問をポータルで完結出来るのは大変体験が良かったです. 途中, 1台サーバーが動かなくなってみんなで青ざめたのですが, 「動かないんスけど...」という問い合わせを投げたらシュッと対応してもらえて助かったりしていました. あと, ベンチマークも実行したらじわじわスコアが上昇していくのが見えて, これがまた脳汁が出てきて... 最高...

例年, 運営非常に大変と思いますが, その甲斐もあってありがたい事に今回もとても楽しませて頂きました. 最後まで頑張れると感想戦も楽しいんだな, ということをようやく知ることが出来ました(?). 学びも多かったので, もし来年も開催されるなら次はちょっと力を入れつつ, 絶対参加したい!!! と思いました. 運営の皆様, 参加者の皆様, お疲れ様でした!!!

最近読んだ本

最近ちょっと仕事が忙しく, 疲弊気味だったので, 「業務に活きそう重点」ではなく「面白そう重点」で, Kindle Unlimitedで配信されている本をいろいろ読んでいました. 軽く感想を書きます.

情報なき国家の悲劇 大本営参謀の情報戦記

第二次世界大戦において, 陸軍の情報参謀を務め, 後に陸上自衛隊の陸将補になった堀栄三氏の著書. 第二次世界大戦において, 突然情報参謀に任じられた筆者が, 如何にして情報分析に従事し, 米軍の侵攻パターンを予測するに至ったかについて振り返っている本です. 単純に読み物としても面白いし(戦後の, ドイツ駐在武官としてキューバ危機に対峙した時のエピソードが面白かった), 今に当てはめれば「内情が見えない競合他社の情報分析」と言えるので, そういった視点でも示唆がある本だと思いました.

血と汗とピクセル: 大ヒットゲーム開発者たちの激戦記

「ディアブロⅢ」, 「アンチャーテッド4」など, 著名なゲームの開発の様子を関係者のインタビューを元に綴った1冊です. 個人的には数年前にハマって遊びまくった「スターデューバレー」も取り上げられていたのが良かったです. ゲーム開発も, 自分が従事するウェブサービス開発のようにエンジニア/プログラマーの働きが必要不可欠な職種だけれど, その働き方, 取り組み方などは違っている部分もあって, 「そういう世界なのか...」と感じることが出来ました. 「スターデューバレー」のように1人で作ったゲームもあれば, 大人数のチームで作ったゲーム, 成功したパターン, うまくいかなかったパターンなど, いろいろな事例が取り上げられていて, 日本の作品は1つもないけれど, 「ゲーム業界の内情」というか, その雰囲気を知ることが出来る良い本だと思いました. ゲーム好きには確実にオススメ出来る1冊だと思います.

他...

現代語訳 信長公記 (新人物文庫)

現代語訳 信長公記 (新人物文庫)

...なんというか, かなり雑多に読んでいますね. 引き続き, ちまちま積ん読を消化していきたいと思います.

最近読んだ本たち

転生したらスプレッドシートだった件

転生したらスプレッドシートだった件

転生したらスプレッドシートだった件

カクヨムから生まれた初の技術評論社の書籍, 「転生したらスプレッドシートだった件」を読みました. 著者は id:minemuracoffee 先生.

仕事でExcelやGoogle Spreadsheetをバリバリ使いこなしているぜ! という方には物足りない内容かもしれませんが, これから使い始める人, 或いは適当に(他の人が作ったシートを参考にしながら...)使っている人にとってはためになる知見が得られそうと思います. ラノベ形式なのでサクサク読める点もありがたいです. ラノベ形式なので(?), 登場人物たちが関数を技名のように呼ぶわけですが, カタカナで呼ぶのは最初「なんとなくダサい感じがあるな...」と思ったりしていました. しかし, よくよく考えると「この関数を口頭で説明するときにはこう呼べばいい」というのがわかって逆に便利(?)だ, という事に途中で気付きました.

...あと,お恥ずかしい話ですが, これまで例えば「A2以降全て」みたいな指定をするときに, A2:A1000 みたいなのをたくさん書いてましたが, これA2:Aとすると良い, というのはこの本で知りました...

個人的には, 索引というか, 「この機能はこの章で取り扱っています」という一覧があれば良かったかな, という気はしています. まあ, 自分の場合は電子書籍版(Kindle)で購入したので, それで検索したら良いのでは? という気はしますが...

外資系コンサルが教える 読書を仕事につなげる技術

同僚の, どなたかのブログで紹介されていて, 面白そう! と思ったところ, Kindle Unlimitedで読めたのでサクッと読んだ本.

著者は外資系コンサルの方なので, 書かれている内容が自分たちのようなエンジニアにそのまま流用出来るか... というとそうではないのですが, その元となる考え方とかは参考になりそう, と思いました.

例えば, ビジネス書において「ビジネス書は狭く深く読む」/「教養書は広く浅く読む」と良い, と紹介されていますが, これをエンジニアに例えるなら「ビジネス書」は言語やアーキテクチャの解説書籍(例えば, 「初めてのPerl」や「エリック・エヴァンスのドメイン駆動設計」など)は狭く深く, 「教養書」は例えば「WEB+DB PRESS」や「Software Design」などの雑誌, 或いは「リーダブルコード」など特定のテーマに沿った本として捉えて, こういったものは広く深く読むと良い... のかな? とか考えることができました.

後は, 割と本を読む時はなるべく全部読もうとするのですが, 「2割だけ読めばいい」と書かれていたのも印象深かったです. この辺りは, ビジネス書や教養書だけでなく, エンジニア向けの本でもそうだと思っていて, なんとなくわかっているつもりでも実践出来ていないのですよね...

ちなみに, 「うまく2割を読む方法」としては, 「目次を見る→総括, 結論といった章があれば読む→これを踏まえて面白そうな章の冒頭を読む」と進み, ピンと来なかったら手を出さない... という方法が紹介されています. 本, 買うと「もったいない」という気持ちで読み進めようとしてしまうので(いつも積んでるのに...), こういう考え方は試してみたいと思いました.

既に読書習慣が構築できていて, うまく本を通してインプットが出来ている人からすれば「そうっッスね」という内容かもしれませんが, そうでない人にとってはいろいろ気付きがある本なのかな, と思いました. そのまま真似をするのではなく, これを参考に仮説を立てて, いろいろ試してみる... という感じで今後の読書に取り組んでいけたらいいなー, と思いました.

他にも...

戦略思考コンプリートブック

戦略思考コンプリートブック

  • 作者:河瀬 誠
  • 発売日: 2003/07/10
  • メディア: 単行本(ソフトカバー)

人生は、運よりも実力よりも「勘違いさせる力」で決まっている

人生は、運よりも実力よりも「勘違いさせる力」で決まっている

  • 作者:ふろむだ
  • 発売日: 2018/08/09
  • メディア: 単行本(ソフトカバー)

去年から今年にかけて, 上記を含めていろいろ本を読んでいたのですが, 感想をまとめるのを忘れてしまっています. 読みながら/読んだ後すぐに感想とか書かないと, どんどん揮発してしまうので気をつけたいですね. せっかくなので折を見て読み返してみたいと思っています.