前提
Rails 4.2系
MySQL 5.6.4以降
問題
同じ値を比較しているはずなのに、こんなことがおきる..!!
expected: "2016-11-22T05:30:12.291Z"
got: "2016-11-22T05:30:12.000Z"
ミリ秒..
結論(僕の場合は)
MySQLのdatetime系のカラムのミリ秒を設定しないまま、
createで作りたてのActiveRecordのインスタンスのdatetime型のattributeと
データベースからロードしてきたActiveRecordのインスタンスのdatetime型のattributeを比較しているのがfailしている原因。
Mysqlでのミリ秒の扱いについて
Mysql5.6.4以降ではdatetime系のカラムでミリ秒もサポートしている。
ただ、普通にmigrationを書くと、defaultではミリ秒は扱わないようになっていて、クエリにミリ秒が含まれていると、秒単位まで四捨五入して保存する。
なので、コードと一緒にかくと以下のようなことがおきる。
now = Time.zone.now # with ミリ秒
hoge = Hoge.new(fuga_datetime: now) # fuga_datetimeはwith ミリ秒
hoge.save # ミリ秒は切り捨てて保存される
hoge.fuga_datetime # インスタンスにはnowがほじされているので with ミリ秒
Hoge.last.fuga_datetime # => DBからのloadなので、without ミリ秒。 式で書くとここでは、hoge.fuga_datetime != UserFood.last.fuga_datetime
こんな感じ。
なんで、たとえば以下のようなget api の specはfailする。
get apiは(当然)データベースから読み込んだインスタンスを使ってレスポンスを返しているのに、
specで比較するときのインスタンスはレコード作成時のインスタンスを使っているため。
let(:hoge) do
create(:hoge, fuga_datetime: Time.zone.now) # hoge.fuga_datetimeはミリ秒をもってしまう
end
subject do
get "some/endpoint/hoges/#{hoge.id}"
end
it do
subject
body_hash = JSON.parse(response.body)
expect(body_hash['hoge']['fuga_datetime']).to eq hoge.fuga_datetime.iso8601(3) # because fuga_datetime has milli-seconds
end
上記の場合は、単にhogeをreloadすれば成功します
it do
subject
hoge.reload # fuga_datetimeをデータベースの値(without ミリ秒)へ
...
end
to_iして比較しろ的なstack over flowがいくつかでてくるんですが、
ミリ秒はmysqlは四捨五入、to_iは切り捨てなのでミリ秒が0.5秒以上になるとfailするのでやっぱりだめ。
it do
subject
body_hash = JSON.parse(response.body)
expect(body_hash['hoge']['fuga_datetime'].to_time.to_i).to eq hoge.to_i # たまに1ずれる
end
やっぱりreloadするのが良さそう。
まとめ
作りたてのインスタンスのdate time型のattributeをspecでかくときはreloadした上で使いましょう
参考
MySQLのミリ秒の扱いについて
https://doruby.jp/users/ap_tg/entries/MySQL_DATETIME_
https://dev.mysql.com/doc/refman/5.6/ja/fractional-seconds.html
Railsでのミリ秒の扱いについて(ちょっと古いけどさくっとまとまってる)