はい。
皆様ご存知、rspec-parameterizedというgemがあります。
複数入力の組み合わせで出力が決まるような、素のまま書くとcontext地獄になってしまうテストを短く書けて便利なやつです。
なんですが、仕事のプロダクトのrequest specにおいて、事前状況セットアップに使う値とその結果のレスポンス内容として期待する値がセットになっているような境界値テストをrspec-parameterizedを使って書いたところ、通ったり通らなかったりする。
数人がかりで2時間弱ほど格闘したところ、下記のことがわかりました。
- 通ったり通らなかったりしたのはDBのDATETIME型が秒単位だったせい
- なのでrspec-parameterizedのせいではない
- rspec-parameterizedはwhereの中身をその場で評価してテストケースの組み合わせを生成していそう
- 遅延評価ではない?
最小の再現サンプルがこちら。 github.com
上記の再現サンプルではコード量を最小にするためにTimecopを使っていますが、この現象を発見した時はRailsのActiveSupport::Testing::TimeHelpersのfreeze_timeとtravel_backを使っていました。
また、before :eachを:suiteにしても変わりませんでした。
ruby-jpのslackにて質問したところ、コミッタのsue445さんより
一応これで回避はできるのですが、自分で書いといてなんだけどすごい気持ち悪いですね…
https://github.com/sue445/rspec-parameterized-with-timecop-issue-minimum-reproduce-sample/commit/6b31bc782b29d007a63928f8ae3bd9e02f253c1dどうやらrspec-parameterizedにパラメータとして渡したDateTime型を返すものはRailsのfreeze_time(またはTimecop)によってDateTimeがモックされる前に評価されてしまうらしいということがわかりました。
これ厳密にはちょっと違ってて、rspec-parameterizedの where ブロックがrspecの before の処理よりも前に評価されるのが原因です。( where の中で let の値が参照できないのも同じ理屈)
という返答をいただきました。
別の方から下記記事を教えていただきましたが今回のケースはrequest specなのでこれも使用できず。
他の方から下記のようにrspecの外側でnowを変数に束縛し、whereブロック中でパラメータとして書く日時はnowからの加減算で表し、テスト時にTimecop.freeze(now)、Railsではtravel_to(now)するという形を提案していただき、これを使うことにしました。
+now = Time.now + RSpec.context 'parameterized spec' do before :each do - Timecop.freeze + Timecop.freeze(now) end after :each do Timecop.return end - subject { Time.now } + subject { now } where(:time) do [ - [Time.now], + [now], ] end
書き方まで完璧とは言えませんが、テストの不確実性は排除できたので一旦よしとします…
反応いただいた皆様ありがとうございました 🙇♂️