Rubyで時間に依存した機能をテストする時便利なgem, Timecop
アプリケーションの機能としてある日時になったら「なになに」する、みたいなものはよくあると思います。例えば、最後にアクセスした日から1ヶ月以上過ぎているユーザにメールを送信する、とか。
こういう機能をテストする場合、どうやってテストしようか色々考えると思います。(DBに登録されている時刻を更新したり、対象の関数に日時を渡せるようにしたり)
そこで、今回紹介するtimecopというgemを使うと今回のようなテストも簡単に行えます。主に以下の2つの関数を使って時間を操作します。
Timecop.freeze
– 指定した時刻に固定するTimecop.travel
– 指定した時刻に移動し、その時点から時間が流れる
サンプルを見たほうが早いと思うので、簡単にPadrinoのアプリケーションをrspecでテストしてみたいと思います。
Padrinoのプロジェクトを作成(rspecを使うように指定)
1 2 3 |
$ padrino g project sample -t rspec $ cd ./sample $ bundle install --path .bundle |
app/app.rbにテスト対象のコードを記述
/currentにアクセスすると現在時刻を返す簡単なものを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
module Sample class App < Padrino::Application register Padrino::Rendering register Padrino::Mailer register Padrino::Helpers enable :sessions # 現在の時刻を返す get :current do Time.now.to_s end end end |
spec/app/app_spec.rb(テストコード)を作成
大きく分けて、Timecopを使わない場合、Timecop.freezeを使った場合、Timecop.travelを使った場合の3つのテストを書いてみます。各コンテキストの最初に1回だけTimecop.freeze, Timecop.travelを実行し時間を操作しています。各exampleでは1秒間のスリープを入れています。(因みにこのサンプルではミリ秒単位のことは考えていないので、場合によってはテストがコケるかも知れません)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# -*- coding: utf-8 -*- require 'spec_helper' describe "app.rbのテスト" do context "Timecopを使わない場合" do it "アクセスした時の時間が表示される" do sleep 1 now = Time.now puts " now: #{now}" get "/current" last_response.body.should eq now.to_s end it "アクセスした時の時間が表示される" do sleep 1 now = Time.now puts " now: #{now}" get "/current" last_response.body.should eq now.to_s end it "アクセスした時の時間が表示される" do sleep 1 now = Time.now puts " now: #{now}" get "/current" last_response.body.should eq now.to_s end end context "時間固定(2014/02/25 15:00:00で固定)" do before :all do Timecop.freeze(Time.new(2014, 2, 25, 15, 0, 0)) end after :all do Timecop.return puts " Timecop.returnのあとのTime.now: #{Time.now}" end it "いつでも、2014/02/25 15:00:00" do sleep 1 get "/current" last_response.body.should eq Time.new(2014, 2, 25, 15, 0, 0).to_s end it "いつでも、2014/02/25 15:00:00" do sleep 1 get "/current" last_response.body.should eq Time.new(2014, 2, 25, 15, 0, 0).to_s end it "いつでも、2014/02/25 15:00:00" do sleep 1 get "/current" last_response.body.should eq Time.new(2014, 2, 25, 15, 0, 0).to_s end end context "時間旅行(2014/02/25 15:00:00にタイムトラベル)" do before :all do Timecop.travel(Time.new(2014, 2, 25, 15, 0, 0)) end after :all do Timecop.return puts " Timecop.returnのあとのTime.now: #{Time.now}" end it "指定した時間の1秒後 2014/02/25 15:00:01" do sleep 1 get "/current" last_response.body.should eq Time.new(2014, 2, 25, 15, 0, 1).to_s end it "指定した時間の2秒後 2014/02/25 15:00:02" do sleep 1 get "/current" last_response.body.should eq Time.new(2014, 2, 25, 15, 0, 2).to_s end it "指定した時間の3秒後 2014/02/25 15:00:03" do sleep 1 get "/current" last_response.body.should eq Time.new(2014, 2, 25, 15, 0, 3).to_s end end end |
テストを実行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ padrino rake spec .... 省略 app.rbのテスト Timecopを使わない場合 now: 2014-02-25 14:54:50 +0900 アクセスした時の時間が表示される now: 2014-02-25 14:54:51 +0900 アクセスした時の時間が表示される now: 2014-02-25 14:54:52 +0900 アクセスした時の時間が表示される 時間固定(2014/02/25 15:00:00で固定) いつでも、2014/02/25 15:00:00 いつでも、2014/02/25 15:00:00 いつでも、2014/02/25 15:00:00 Timecop.returnのあとのTime.now: 2014-02-25 14:54:55 +0900 時間旅行(2014/02/25 15:00:00にタイムトラベル) 指定した時間の1秒後 2014/02/25 15:00:01 指定した時間の2秒後 2014/02/25 15:00:02 指定した時間の2秒後 2014/02/25 15:00:03 Timecop.returnのあとのTime.now: 2014-02-25 14:54:58 +0900 |
この結果を見るとわかると思いますが、Timecop.freeze
は指定した時刻で時間を固定します。freezeしたあとのどのタイミングでTime.now
をしてみてもfreezeした時の時刻が取得できると思います。
Timecop.travel
の場合はといいますと、指定した時刻から時間が流れている感じになりますので、1秒後にTime.now
としますと指定した時刻+1秒後の時刻が取得できると思います。
afterで実行しているTimecop.return
は時刻を元に戻すためですので、テストの終わりに必ず実行するようにしておくといいでしょう。
また、Time.now
以外にもDate.today
、DateTime.now
も同じように偽装されますので、それらを利用している関数のテストにも使うことが出来ます。
ということで、Timecopを使うとテストがシンプルに書けそうですね。