Perl でテストを書いてるみなさん。 Test2 にはもう慣れましたよね?私は全然慣れてません。
ということで、最近、個人のプロジェクトで書かれてるテストを Test2 にしました。
その際、それまでのテストを Test::Spec で書いていたので、Test::Tools::Spec に移行させることにしました。
Test::Spec
は Ruby の Rspec 風に書くことができるモジュールです。
前提として Perl 5.40 での話で、Perlのバージョンによっては Test2
の最新を cpanm などでインストールする必要があります。(5.40 から Test2::Suite
がコア入りした)
さくっと置き換えられたもの
use
文
use Test::Spec;
を消して use Test2::V0;
se Test2::Tools::Spec;
を追加
-use strict; -use warnings; -use utf8; - -use Test::Spec; +use 5.40.0; +use Test2::V0; +use Test2::Tools::Spec;
runtest
runtests unless caller
を消して done_testing
を追加
そのまま置き換えられるけど、それぞれ意味することは違うので、理解したうえで置換。
-# XXX: forkprove has caller -# runtests unless caller; -runtests(); +done_testing;
context
s/context/describe/g
する
- context 'case call run method without arguments' => sub { + describe 'case call run method without arguments' => sub { it 'when returns undef' => sub { my $res = Oden::Command::AYT->run(); is $res, undef; }; };
before
, after
,
all, each まで含めた関数名になり、名前が必要になる。単純に置換 s/before\sall/before_all "XXX"/
して後で埋めるのでもよい。
- before all => sub { + before_all "setup" => sub {
- context 'case non-empty message' => sub { - before all => sub { + describe 'case non-empty message' => sub { + before_each "create_test_message" => sub {
- after all => sub { + after_all "cleanup"=> sub {
around
基本的に before, all とやることは同じ。yield()
は shift->()
に置換しても良いが、変数に逃がしておくと安全
- context "case thread_id is not set" => sub { - around { + describe "case thread_id is not set" => sub { + around_all "mockup" => sub { + my $tests = shift; $hash->{api} = Oden::API::Discord->new(token => $hash->{valid_token}, interval => 0); - yield; + $tests->(); delete $hash->{api}; };
stubs
(mock)
しっかり書き換えが必要。
これは Test2::Tools::Spec
ではなく、Test2::Tools::Mock
に機能がある。
use Test2::V0
するだけで読み込んでくれる。
Modiule->stubs(+{
を mock "Modiule" => (
にしつつ、override
配列にいれる必要がある。
動的にメソッドが生えるものは override
できないので注意。
Moops
の場合は add
で通ったがダメなパターンもあるかもしれない。
overrideの例
$hash->{api} = Oden::API::Discord->new(token => $hash->{invalid_token}, interval => 0); - $hash->{stubs}->{furl} = Furl->stubs(+{ - request => sub { - Furl::Response->new( - 1, - '401', - "Unauthorized", - +{ - 'content-type' => 'application/json' - }, - q|{"message": "401: Unauthorized", "code": 0}| - ); - }, - }); - + $hash->{mocks}->{furl} = mock "Furl" => ( + override => [ + request => sub { + Furl::Response->new( + 1, + '401', + "Unauthorized", + +{ + 'content-type' => 'application/json' + }, + q|{"message": "401: Unauthorized", "code": 0}| + ); + }, + ], + );
add の例。
- $hash->{stubs} = AnyEvent::Discord->stubs( - token => sub { return 'your token'; }, - user_agent => sub { return 'Perl-AnyEventDiscord/' . shift->VERSION }, - _ws_send_payload => sub { return $_[1]; }, - _debug => sub { }, - _discord_api => sub { +{ url => "wss://gateway.discord.gg/" } }, + $hash->{discord} = AnyEvent::Discord->new; + my $mock = mock "AnyEvent::Discord" => ( + add => [ + token => sub { return 'your token'; }, + user_agent => sub { return 'Perl-AnyEventDiscord/' . shift->VERSION }, + _ws_send_payload => sub { return $_[1]; }, + _debug => sub { }, + _discord_api => sub { +{ url => "wss://gateway.discord.gg.example/" } } + ], );
hash などに複数いれる場合は ()
しておくと安全
mock "Hoge" => ( ... ),
ではなく mock("Hoge" => ())
にしておく。
こうすることで 2個目のmock() が1個目の mock()の第三引数として扱われないようにする。
- before all => sub { - $hash->{stubs} =+{ - connect => AnyEvent::WebSocket::Client->stubs( - connect => sub { - $hash->{called}->{connect} = 1; - return AE::cv; - }, - ), - recv => AnyEvent::CondVar->stubs( - recv => sub { - $hash->{called}->{recv} = 1; - }, - ), + + before_all setup => sub { + $hash->{mocks} =+{ + connect => mock("AnyEvent::WebSocket::Client" => ( + override => [ + connect => sub { + $hash->{called}->{connect} = 1; + return AE::cv; + }, + ], + )), + recv => mock("AnyEvent::CondVar" => ( + override => [ + recv => sub { + $hash->{called}->{recv} = 1; + }, + ], + )), }; };
they
ない。 Test::Spec#they
は Test::Spec#it
のエイリアスなので素直に it 使おう。
ちなみに 、Test2::Tools::Spec#it
は Test2::Tools::Spec#tests
のエイリアスなので、it()
も they()
も tests()
にするのが正解かもしれない。
ハマったこと
describe/it
Test::Speck でよく書く describe
> it
や describe
>describe(context)
> it
の構造について、 構造は正しく保ちますが、実行順は順不同になります。
Test::Spec
で context
1 ,context
2 の順で動くことを前提で書いていたテストが Test2::Tools::Spec
で通らなくなったので気付いた。
これは、そもそもテスト順に依存したテストを書くなという話で、Test::Spec
で「たまたま許されていた」と受け止めている。
以下にコード例と動作例を挙げておきます
コード例
describe 'about describe' => sub { my $hash; describe '1st describe' => sub { it '1st it' => sub { ok 1; }; it '2nd it' => sub { ok 2; }; it '3rd it' => sub { ok 3; }; }; describe '2nd describe' => sub { describe '2-1 describe' => sub { it '2-1 1st it' => sub { ok '2-1-1' }; it '2-1 2st it' => sub { ok '2-1-2' }; it '2-1 3rd it' => sub { ok '2-1-3' }; }; describe '2-2 describe' => sub { it '2-2 1st it' => sub { ok '2-2-1' }; it '2-2 2st it' => sub { ok '2-2-2' }; it '2-2 3rd it' => sub { ok '2-2-3' }; }; }; }; done_testing();
実行結果
./t/test.t .. # Seeded srand with seed '20241212' from local date. ok 1 - about describe { ok 1 - 2nd describe { ok 1 - 2-2 describe { ok 1 - 2-2 2st it { ok 1 1..1 } ok 2 - 2-2 3rd it { ok 1 1..1 } ok 3 - 2-2 1st it { ok 1 1..1 } 1..3 } ok 2 - 2-1 describe { ok 1 - 2-1 3rd it { ok 1 1..1 } ok 2 - 2-1 2st it { ok 1 1..1 } ok 3 - 2-1 1st it { ok 1 1..1 } 1..3 } 1..2 } ok 2 - 1st describe { ok 1 - 3rd it { ok 1 1..1 } ok 2 - 2nd it { ok 1 1..1 } ok 3 - 1st it { ok 1 1..1 } 1..3 } 1..2 } 1..1 ok All tests successful.
実行結果 別例
describe
> it
だけの場合でも起きます。
こちらは実行例だけで伝わると思うのでコードは省略してます。
./t/test.t .. # Seeded srand with seed '20241212' from local date. ok 1 - about describe { ok 1 - 4th it { ok 1 1..1 } ok 2 - 1st it { ok 1 1..1 } ok 3 - 2nd it { ok 1 1..1 } ok 4 - 3rd it { ok 1 1..1 } ok 5 - 5th it { ok 1 1..1 } ok 6 - 6th it { ok 1 1..1 } 1..6 } 1..1 ok All tests successful.
あきらめたもの
xcontext
, xdescriber
, xit
ない。 個人的にはコメントアウトするより楽なので使ってただけで、コメントアウトに戻すだけの運用になった。 TODO ブロック代わりに使ってると、面倒臭いかもしれない。
share()
, shared_examples_for()
, it_should_behave_like()
ない。
今回は shared_examples_for()
, it_should_behave_like()
を使ってなかったので、 share()
を消すだけの変更になった。
describe 'about XXX' => sub {
my $hash;
- share %$hash
どうしても shared_examples_for()
, it_should_behave_like()
みたいなことをしたい場合、関数に逃がすしか無さそう。
その際は share()
に入るべきリファレンスも一緒に渡す感じになる。(あるいは Test::Spec::SharedHash
を再実装するか)
Test2::Tools::Spec
以外の話
ついでなので変えたところを挙げていく。 (厳密に言えば stubs(mock) もココに入るが……)
Test::Exception
手を入れれば use Test2::V0
だけで行けます。実態は Test2::Tools::Exception
にある。
it 'throw exception' => sub { - throws_ok { + my $throws = dies { Oden::API::Discord->new() - } qr/require token parameter/; + }; + like $throws, qr/require token parameter/; };
Test2::Tools::Exception
の SYNOPSIS には
like( dies { die 'xxx' }, qr/xxx/, "Got exception" );
と記載されてるけど 若干見づらい 好みの問題で2行に分けてます。
my $throws = dies { die 'xxx' }, like(throws, qr/xxx/, "Got exception");
Test:Warn
warn は exception と違って use Test2::Tools::Warnings
が必要。
+use Test2::V0; +use Test2::Tools::Spec; +use Test2::Tools::Warnings qw/waning warnings/; -use Test::Spec; -use Test::Exception; -use Test::Warn;
使い方は基本的に Exception と一緒。
- warning_like { + my $warning = warning { my $package = Oden::Dispatcher->dispatch('not_exist'); - is $package, undef, 'undef'; - } qr/Can't locate Oden\/Command\/NotExist.pm in \@INC/, 'throw and warning'; + }; + like $warning , qr/Can't locate Oden\/Command\/NotExist.pm in \@INC/, 'throw and warning';
、warning が複数出るケースの場合はちょっと面倒
- warnings_like { + my $warnings = warnings { my $res = $hash->{api}->send_attached_file( $hash->{valid_channel_id}, $hash->{path} ); is $res, false; - } [qr/401/, qr/Unauthorized/]; + }; + + like $warnings->[0], qr/401/; + like $warnings->[1], qr/Unauthorized/;
isa_ok
isa_ok が多重継承に対応した。基本配列型にすれば大丈夫
- isa_ok $api, 'Oden::API::Discord'; + isa_ok $api, ['Oden::API::Discord'];
HASH, ARRAY を期待する場合は ref_ok()
にする必要がある
my $discord = $hash->{discord}; my $endpoints = $discord->endpoint_config(); - isa_ok $endpoints, 'HASH'; + ref_ok $endpoints, 'HASH';
is_deeply
Test2 の is で構造体比較ができるようになったので、 is_deeply は is に置き換えて行ける。
実態は Test2::Tools::Compare#is
- is_deeply $endpoints, +{ + is $endpoints, +{
最初は以下の様に Test2::Tools::ClassicCompare#is_deeply
を use していたが is()
で解決する内容だった。
+use Test2::Tools::ClassicCompare qw/is_deeply/;
explain
テスト中のデバッグ出力の神。無いと生きていけないが、基本 pushする前に消すので、普段 Test::More
でテスト書いてても存在を忘れられがち
Test2::Tools::Explain
にあるのでデバッグ出力が必要なときだけ use
する。
use Data::Dumper;
して Dump
でもいいけど。
+ use Test2::Tools::Explain;
...
diag explain $hash
まとめ: というか diff 見たほうが早いって人向け
Test::Spec から Test2::Tools::Spec に移行した p-r があるのでそのコード見てください 該当リポジトリ内のコードの大部分を最近別のリポジトリに移行したので最新をみるときはそちらを。(diff の有る方のリポジトリから最新を見ようとすると not found になってます。🙇)