tsucchi’s diary(元はてなダイアリー)

はてなダイアリー(d.hatena.ne.jp/tsucchi1022)から移行したものです

Time::Fake もいいけど、これからは Test::MockTime だね

なんとなく、cpanRSS 更新フィードを眺めていたら、Test::MockTimeというモジュールを見つけました。

Time::Fake と同じように、Perl の時計を設定変更しちゃうモジュールなのですが、こいつは「set_fixed_time()」というメソッドを持っていて、文字通り「時間を固定」できます。

Time::Fake だと、高負荷時に期待している秒数より 1 秒ずれてしまうことが時々あって、困っていました。無理やり再現するケースを作ると、たとえばこんな感じ。

#!/usr/bin/perl
use strict;
use warnings;
use Time::Local;
use Test::More 'no_plan';
use Time::Fake;

my $time_for_test = timelocal(56, 34, 12, 22, 10-1, 2008);# 2008/10/22 12:34 56秒;
my $num_tests = 1000;
my $load = 5000; #負荷かけ

no warnings 'void';

for ( (1 .. $num_tests) ) {
    Time::Fake->offset($time_for_test);
    sqrt( rand() ) for ( (1 .. $load) );#何でもいいけど負荷のかかる演算をする
    is('Wed Oct 22 12:34:56 2008', "" .localtime());
}

sqrt( rand() )は高負荷時を想定してます。Perl のプログラム内よりも、どちらかというと開発環境で負荷が上がった場合なんかを考えているわけです。やってみると、

tsucchi@appears[103]% prove a.pl
a.......126/?
#   Failed test at a.pl line 30.
#          got: 'Wed Oct 22 12:34:56 2008'
#     expected: 'Wed Oct 22 12:34:57 2008'
a.......334/?
#   Failed test at a.pl line 30.
#          got: 'Wed Oct 22 12:34:56 2008'
#     expected: 'Wed Oct 22 12:34:57 2008'
a.......543/?
#   Failed test at a.pl line 30.
#          got: 'Wed Oct 22 12:34:56 2008'
#     expected: 'Wed Oct 22 12:34:57 2008'
a.......751/?
#   Failed test at a.pl line 30.
#          got: 'Wed Oct 22 12:34:56 2008'
#     expected: 'Wed Oct 22 12:34:57 2008'
a.......960/?
#   Failed test at a.pl line 30.
#          got: 'Wed Oct 22 12:34:56 2008'
#     expected: 'Wed Oct 22 12:34:57 2008'
# Looks like you failed 5 tests of 1000.
a....... Dubious, test returned 5 (wstat 1280, 0x500)
 Failed 5/1000 subtests

Test Summary Report
-------------------
a.pl (Wstat: 1280 Tests: 1000 Failed: 5)
  Failed tests:  126, 334, 543, 752, 961
  Non-zero exit status: 5
Files=1, Tests=1000,  5 wallclock secs ( 0.03 usr  0.03 sys +  4.97 cusr  0.08 csys =  5.11 CPU)
Result: FAIL

こんな感じで、高負荷時(?)に「ときどき」失敗します。(ちなみに、オイラよりいい CPU を使っている人はまったく失敗しないかもしれない。そういう人は $load を増やしてみてください)

で、これを Test::MockTime に差し替えます。こんな感じ。

#!/usr/bin/perl
use strict;
use warnings;
use Time::Local;
use Test::More 'no_plan';
use Test::MockTime;

my $time_for_test = timelocal(56, 34, 12, 22, 10-1, 2008);# 2008/10/22 12:34 56秒;
my $num_tests = 1000;
my $load = 5000; #負荷かけ

no warnings 'void';

for ( (1 .. $num_tests) ) {
    Test::MockTime::set_fixed_time($time_for_test);
    sqrt( rand() ) for ( (1 .. $load) );#何でもいいけど負荷のかかる演算をする
    is('Wed Oct 22 12:34:56 2008', "" .localtime());
}

Time::Fake->offset を Test::MockTime::set_fixed_time に差し替えただけです。
で、こいつを実行すると、

tsucchi@appears[106]% prove a.pl
a.......ok
All tests successful.
Files=1, Tests=1000,  5 wallclock secs ( 0.00 usr  0.03 sys +  4.89 cusr  0.03 csys =  4.95 CPU)
Result: PASS

まあ「固定」してるわけだから、当然 OK ですな。offset を set_fixed_time に変えればそのまま動くので書き換えも楽々でした。

ちなみに、どちらのモジュールも *CORE::GLOBAL::time() などをいじっているので、同時に使おうとすると訳分からん事態になりますのでご注意を。

結論

時間に関わるテストをするときは、Test::MockTime::set_fixed_time() を使いましょう!

...で、いいのかな。ぐぐってもどちらのモジュールもあんまり使っている人いないんだよね。みんなどうやっているんだろ???

もっといいモジュールや、「定番」のようなモジュールや、あるいはモジュールじゃなくてもベストプラクティス的なものがあるなら教えてほしいです。

追記。。。的なもの(2010/11/20)

なんか一年以上前の記事に急にブクマがついててびっくりしました。

miyagawa さんから、ブクマコメントもいただきましたのでこちらにも載せておきます。

miyagawa そもそもTime::Fakeは固定するんじゃなくてoffsetをずらすだけだから、clockがずれたら結果がずれるのあたりまえじゃないかな

そう、当たり前なんですよね。

Time::Fake を使ってた頃は、Test::MockTime を知らなかったし、また CORE::time をいじる方法も知らなかったので、「何でもいいから、現在時刻をいじらせてくれ〜」という気持ちで Time::Fake を使っていたのでした。これは正しくない使い方ですよね。。。

時間に関するテストをするにあたって、現時点での僕の結論は、

  1. まずはなるべく現在時刻に依存しないようにテストを書くこと
  2. どうしても必要なら Test::MockTime を使うこと

といった感じです。

分かってる人なら、CORE::time を直接書き換えるのもアリといえばアリですが、チーム開発している場合は、Test::MockTime のようなモジュールを使うほうが意図が明確で分かりやすいのではないかな、と思っています。

また、Time::Fake は、たとえば、「2038年問題が出ないかテストしたい」とか、そういった用途(PC の時計をいじらずに、Perl だけ時間をずらしてテストしたい場合)ならテスト用途もありそうですが、通常のテストコードではあまり使えないのではないかな、と思っています。

see also: Time::Fake 可愛いよ、Time::Fake、 - tsucchiの日記