Edited at

datetime型のrspec書くときにミリ秒単位で値がずれてfailすることがある問題について

More than 1 year has passed since last update.


前提

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でのミリ秒の扱いについて(ちょっと古いけどさくっとまとまってる)

http://qiita.com/joker1007/items/d4043eced880cbbce21c