先日TimeZone 周りでハマったので、備忘用にメモします。
結論
Time.zone.parse('2019-01-01')
と2019-01-01
は異なる。*1ActiveSupport::TimeZone
とString
での時間の比較は注意が必要。Time.zone.parse
は比較対象もTime.zone.parse
にするのがbetter。
内容
先日RoR で書かれたとあるコードのリファクタリングを行う機会がありました。 メインロジックの実装を完了し、Rspec でテストコードを実装していました。 その日は珍しく集中力が続き、スラスラとテストコードの実装が進みました。 が、それも束の間
$ bundle exec rspec spec/helpers/test_helper.rb F Failures: 1) TestHelper#on_sale? when the released date is on/past 2019-01-01 should be true Failure/Error: expect(on_sale?(album)).to be true expected #<TrueClass:20> => true got #<FalseClass:0> => false Compared using equal?, which compares object identity, but expected and actual are not the same object. Use `expect(actual).to eq(expected)` if you don't care about object identity in this example. # ./spec/helpers/test_helper.rb:10:in `block (4 levels) in <top (required)>' Finished in 0.22805 seconds 1 example, 1 failure Failed examples: rspec ./spec/helpers/test_helper.rb:9 # TestHelper#on_sale? when the released date is on/past 2019-01-01 should be true
Rspec が落ちました。 ソースを確認してみると、
module TestHelper def on_sale?(album) album.released_at >= '2019-01-01' end end
require 'spec_helper' describe TestHelper do describe '#on_sale?' do let!(:album) { create(:album, released_at: Time.zone.parse('2019-01-01')) } context 'when the released date is on/past 2019-01-01' do it 'should be true' do expect(on_sale?(album)).to be true end end end end
Time.zone.parse('2019-01-01') >= 2019-01-01
がfalse
( ´Д`)y━・~~
・・・・・
ペーペーの僕には全く理由が分かりませんでした。
そこで、困った時のbin/rails c
です。
$ bin/rails c pry(main)> t1 = Time.zone.parse('2019-01-01') => Mon, 01 Jan 2018 00:00:00 JST +09:00 >= Mon, 01 Jan 2018 00:09:00 JST +09:00 pry(main)> string_time = '2019-01-01' => "2019-01-01" pry(main)> t1 >= string_time => false
t1
はActiveSupport::TimeWithZone
クラスで、t2
はString
クラスな訳で、、、
この時の脳内は、
- Ruby(Rails) でそもそも
ActiveSupport::TimeWithZone
とString
は比較できない!? - 比較できたとして、t2("2019-01-01") は何時なのか?
String
の場合00:00:00
じゃない!?
みたいな感じでした。
そこで、引き続きコンソール叩いてみました。
pry(main)> t2 = Time.zone.parse('2019-01-01 23:59:59') pry(main)> t2 >= string_time => true pry(main)> t3 = Time.zone.parse('2019-01-01 12:00:00') pry(main)> t3 >= string_time => true pry(main)> t4 = Time.zone.parse('2019-01-01 06:00:00') pry(main)> t4 >= string_time => false pry(main)> t5 = Time.zone.parse('2019-01-01 09:00:00') pry(main)> t5 >= string_time => true
この辺りで気がつきました。TimeZone
の存在に、、、何やってんだよって感じですよね、本当に。
pry(main)> '2019-01-01'.to_time => 2019-01-01 00:00:00 +0000
やっぱり、 +0000
ってなってる。String
の場合、UTC(協定世界時)で時間が設定されるようです。
一方、Time.zone.parse('2019-01-01')
の方はJST で設定されています。
皆さんご存知のように、JST はUTC に比べて9時間早いので、今回の比較は実際には下記の通りです。
Mon, 01 Jan 2018 00:00:00 JST +09:00 >= Mon, 01 Jan 2018 00:09:00 JST +09:00
これでは、true
になるはずがありませんね。
そこで、今回は下記の通り変更しました。
module TestHelper def on_sale?(album) - album.released_at >= '2019-01-01' + album.released_at >= Time.zone.parse('2019-01-01') end end
require 'spec_helper' describe TestHelper do describe '#on_sale?' do let!(:album) { create(:album, released_at: Time.zone.parse('2019-01-01')) } context 'when the released date is on/past 2019-01-01' do it 'should be true' do expect(on_sale?(album)).to be true end end end end
この箇所に限らずJST を基本的に使っているので、今回のように急にUTC が出てきて予期せぬバグが出ないようにメインロジックをTime.zone.parse
を用いて修正しました。
$ bundle exec rspec spec/helpers/test_helper.rb . Finished in 0.35198 seconds 1 example, 0 failures
無事テストもパスしました。( ´∀`)
ちなみに、このTimeZone
ですがconfig/application.rb
で設定できます。
class Application < Rails::Application ... config.time_zone = 'Tokyo' # ここ ... end
参考:
所感
- テストを書くことって大事ですね。最近テストコードの重要性をひしひしと感じていますが、今回さらに強く思いました。プログラミング始めたての頃は動けばOK みたいな感じでやってましたが、今思えば恐ろしいことです。
Time.zone
と何回もタイピングしていながら、TimeZone に気がつかなかったのはとても悔しいです。そして、このエントリーを書きながら自分へっぽこぶりにつくづく嫌になってきました。- とっても遠回りをしてしまったけれど、仮説検証のサイクルを回せたのはよかったかなぁと。Lv. 3 からLv. 4 くらいにはなったかなぁと。