Rubyで時間に依存した機能をテストする時便利なgem, Timecop
アプリケーションの機能としてある日時になったら「なになに」する、みたいなものはよくあると思います。例えば、最後にアクセスした日から1ヶ月以上過ぎているユーザにメールを送信する、とか。
こういう機能をテストする場合、どうやってテストしようか色々考えると思います。(DBに登録されている時刻を更新したり、対象の関数に日時を渡せるようにしたり)
そこで、今回紹介するtimecopというgemを使うと今回のようなテストも簡単に行えます。主に以下の2つの関数を使って時間を操作します。
Timecop.freeze
– 指定した時刻に固定するTimecop.travel
– 指定した時刻に移動し、その時点から時間が流れる
サンプルを見たほうが早いと思うので、簡単にPadrinoのアプリケーションをrspecでテストしてみたいと思います。
Padrinoのプロジェクトを作成(rspecを使うように指定)
$ padrino g project sample -t rspec
$ cd ./sample
$ bundle install --path .bundle
app/app.rbにテスト対象のコードを記述
/currentにアクセスすると現在時刻を返す簡単なものを作成します。
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秒間のスリープを入れています。(因みにこのサンプルではミリ秒単位のことは考えていないので、場合によってはテストがコケるかも知れません)
# -*- 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
テストを実行
$ 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を使うとテストがシンプルに書けそうですね。