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 さんです. よろしくおねがいします.