はじめに
このメモは、Railsのspecを書いていた時に、どうしてもテストが通らなくてハマった点について書いています。
内容としては表題の通りですが、かいつまんで言うと以下のような感じです。
前提条件
- DateTime型の列に、その月の最終日の値を設定する
- その月の最終日、かつ、できれば最終時刻を格納したい
- Railsの日付関数 end_of_day を使って最終時刻を出し、その値を格納する
書いたテスト
- その日の最終日時を生成 ( Exp. a = Time.current.end_of_day )
- 1の値をテーブルに格納, save ( Exp. xxxx.closed = a )
- save後に1の値を含むインスタンスを取り出して、1の値 (Exp. expect(xxxx.closed).to eq a )
単純にテストした結果
単純に比較したら、ミリ秒単位で差が生じてエラーになってしまいました....
こんなメッセージが。
expected: 2016-12-31 23:59:59.999999999 +0900
got: 2016-12-31 23:59:59.000000000 +0900
失敗した理由
saveしてDBに格納される前は、精度は落ちません。
saveしてデータを取り出すと、ミリ秒分は0で埋まってしまいます。
知らなかった自分が悪いのですが、MySQLのDATETIME型はミリ秒省略されてしまうようで、例えば23:59:59.9999 の元データを生成したとしても、 23:59:59.000 で入るんですね...
今回、DoRuby様の記事が大変参考になりました。ありがとうございました!
- [「MySQL DATETIME型のミリ秒の扱いについて」] (https://doruby.jp/users/ap_tg/entries/MySQL_DATETIME_)
- https://doruby.jp/users/ap_tg/entries/MySQL_DATETIME_
とりあえずの対応
save前のデータもsave後に取り出したデータも、型はActiveSupport::TimeWithZoneで同じなのですが、精度が違っているため単純に eq ができませんでした。
結局、to_sで文字列に変換して比較をしています....
it '対象月のレコードのcloseは対象月の月末になる' do
#
# NOTE: MySQLのDatetimeはミリ秒が省略されるため、
# 23:59:99:99 -> 23:59:00:00に丸められてしまう
# 文字列で比較を実施
expect(xxxx.closed.to_s).to eq target_month.end_of_month.end_of_day.to_s
end
まとめという名の感想
今回はMySQLなので、このような状況になっています。他のDBに関しては試していないのですが、いろいろ差異はありそう。DBの際や制約もうっかりして気がつかないでいると、妙なところでハマるんですね。
Railsに限った話ではないですが、何か日付の最終時刻まで有効といった条件は、「その日のend_of_day」 ではなく 「翌日のbeginning_of_dayより小さい」という条件に設定したほうが安全そうだ...というのも納得した次第です。
恥ずかしながらのメモでした。
20170203 追記:
DB的な精度の関係でどうしても丸められてしまう場合、上記の例では to_s で対応してみましたが、ある範囲内での誤差を許容したテストを書きたい場合は、こんなものを利用します。
- minitestの場合: assert_in_delta
- rspecの場合: be_within
書き直してみると、こんな感じでしょうか。
it '対象月のレコードのcloseは対象月の月末になる' do
#
# NOTE: MySQLのDatetimeはミリ秒が省略されるため、
# 23:59:99:99 -> 23:59:00:00に丸められてしまう
# be_withinで誤差を許容して比較
expect(xxxx.closed).to be_within(1.second).of(target_month.end_of_month.end_of_day)
end