用 Timecop 自由穿梭時間軸

Tony Hsu
5 min readJan 3, 2017

--

測試中有時會遇到時間敏感 (time-sensitive) 的例子,如果沒有妥善地處理就等於開了大門引進了執行測試時間的不穩定因素,演變成測試碼在上午執行是正確的,下午卻是錯誤的尷尬情況。而最常見造成 time-sensitive 的原因是Time.now這個 api,因為時間是一個相對的概念,不同的時間點去呼叫now所得到的值都不相同。

舉一個簡單的 time-sensitive 例子,如果要根據預約時間 (reserved_at) 查詢在今天之內的訂位 (Reservation) 的資料,查詢條件會是固定的區間,也就是介於 today 到 tomorrow 的區間,是相對穩定的條件。但是如果改要查詢今天之內,但還沒有到達預定時間 (reserved_at) 的訂位,這時候查詢的條件就會介於 Time.now 到 tomorrow 的區間。如果測試環境中有兩筆訂位資料預約時間分別在今天午餐 (12:00) 跟今天晚餐 (19:00),就會因為執行測試的時間不同,查詢到不同的資料 (早上執行會查詢到兩筆,下午執行只會查詢到一筆)。

有些 time-sensitive 的狀況可以在測試碼中先設定一個提供參考標準的標準時間,然後在測試中所有與時間相關的參數都去對應標準時間,但是這個解法並不能完全解決Time.now所帶來的不穩定性。另外一個常見的做法,則是在測試中對Time進行 mock,或是 stub now 這個 method (當然也可以 stub 其他造成time sensitive 的 method),如此一來,我們就可以巧妙地設定要回傳時間的值。不過,mock 物件的生命週期只存在於一個 example,所以說如果有多個 test cases,就必須要一個個去 mock or stubbed。這時候就會非常希望自己能有隨心所欲自由穿梭時間的超能力,Timecop 這個 gem 沒辦法給你穿越時空的超能力,但可以很方便地幫你處理這類型的問題。

使用 Timecop 只需記住三個 function 分別是 freezetravelscale

時間停止 freeze ,在 block scope 中的時間都會停留在指定的時間,或是直到你返回原來的時間。

puts Time.now #=> 2017-01-03 19:21:09 +0800Timecop.freeze(Time.zone.local(2000,1,1)) do
puts Time.now #=> 2000-01-01 00:00:00 +0800
end
puts Time.now #=> 2017-01-03 19:21:09 +0800Timecop.freeze(Time.zone.local(2000,1,1))
puts Time.now #=> 2000-01-01 00:00:00 +0800
Timecop.return
puts Time.now #=> 2017-01-03 19:21:09 +0800

時間穿梭 travel ,會移動到相對的時間並且停止

Timecop.freeze(Time.zone.local(2000,1,1)) do
puts Time.now #=> 2000-01-01 00:00:00 +0800
Timecop.travel 2.hours do
puts Time.now #=> 2000-01-01 02:00:00 +0800
Timecop.travel 10.hours do
puts Time.now #=> 2000-01-01 12:00:00 +0800
end
end
end

從上面範例就能知道,Timecop 是可以層層串接,每一層會有自己的時間。

時間伸縮 scale ,可以伸縮時間的尺寸,一秒當一小時過

Timecop.scale(3600) do # 1秒當3600秒(小時)
puts Time.now #=> 2017-01-03 19:41:19 +0800
sleep 12 # 過了12秒,其實過了12小時
puts Time.now #=> 2017-01-04 07:41:39 +0800
end

在 Rspec 中,可以在 before hook 進入到指定的時間點,接著在 after hook 中脫離 Timecop 的時間,所以在 before 跟 after 之中的區間所執行的code 都會在 Timecop 的時間中。

before(:all) do 
Timecop.freeze(Time.zone.local(2000,1,1))
end
after(:all) do
Timecop.return
end

其實 Rspec 還有一個大家比較少用的 around hook,用法就是在每個example (也就是 it) 前後執行。如果 Timecop 是在 around hook 中執行,也就是說在 example 的前後,進出 Timecop 的時間。當然如果你要在各個 example 以不同時間去驗證結果,就幫每個 example 穿梭一下吧。

around do |example| 
Timecop.freeze(Time.zone.local(2000, 1, 1), &example)
end
it 'queries two records when morning' do
Timecop.travel 10.hours do #=> 2000-01-01 10:00:00 +0800
expect(subject.size).to eq(2)
end
end
it 'queries one record when afternoon' do
Timecop.travel 15.hours do #=> 2000-01-01 15:00:00 +0800
expect(subject.size).to eq(1)
end
end

有了 Timecop 的幫忙,在測試的時候大家都能像90年代的電影 ‘Back to the Future’ 一樣,穿梭時光回到未來。

--

--