這是 Test Prescription 這本書部分內容的筆記整理,如果你對這篇文章有興趣,強烈建議你去讀讀這本書。
首先,我們建立了一些project的fixtures做為測試資料:
spec/fixtures/projects.yml
runway:
name: Project Runway start_date: 2015-01-20
greenlight:
name: Project Greenlight start_date: 2015-02-04
gutenberg:
name: Project Gutenberg start_date: 2015-01-31
我們有一個需求是要根據給的時間找出最近的project:
spec/models/project_spec.rb
it "finds recently started projects" do
actual = Project.find_recently_started(6.months)
expect(actual.size).to eq(3)
end
我們在2015/01/20寫了程式並讓程式pass上面的測試:
def self.find_recently_started(time_span)
old_time = Date.today - time_span
all(conditions: ["start_date > ?", old_time.to_s(:db)])
end
結果過了六個月後的某天,這個測試竟然爆了,而在那時候搞不好我們都已經忘了這個測試在幹嘛…爆的原因很明顯,因為我們用了固定的時間,而「時間」是會隨著時間變動的。要解決上面的問題,有兩種方式:
要解決固定時間造成的問題,最直覺的做法就是改用相對時間,如下面所示:
spec/fixtures/projects.yml
runway:
name: Project Runway start_date: <%= 1.month.ago %>
greenlight:
name: Project Greenlight start_date: <%= 1.week.ago %>
gutenberg:
name: Project Gutenberg start_date: <%= 1.day.ago %>
factory也可以做同樣的事:
spec/factories/projects.rb
factory :project do
name "Project Runway"
start_date { 1.week.ago }
end
相對時間的確可以解決一開始遇到的問題,只是在某些情況下我們很難用相對時間來做測試。例如我們要測試某個project被建立時,它的建立時間是否有被存起來:
spec/models/project_spec.rb
it "should store the created_at time when a project is created" do
project = Project.create(name: 'New project')
expect(project.created_at.to_s(:db)).to eq(Time.now.to_s(:db))
end
因為時間是流動的,所以在「建立project的時間」與「驗證建立的時間」中間仍然會有時間差,就會造成上面的測試失敗。這時候就要使用假時間來做測試了。
在rails 4.1之後提供了一個處理時間測試的helper - ActiveSupport::Testing::TimeHelpers,它可以針對Date、Time、DateTime或是任何支援.to_time的class設定假時間。在rails 4.1之前可以使用Timecop這個gem達到相同的效果。
設定假時間有下面幾種method:
功用 | ActiveSupport::Testing::TimeHelpers的語法 | Timecop的語法 |
---|---|---|
將現在時間設定成指定的時間(given_time)並暫停時間,也就是在測試過程中時間不會流動,Time.now的值是固定的。 | travel_to(given_time) | freeze(given_time) |
將現在時間設定成指定的時間(given_time),並讓時間繼續流動。 | travel(given_time) | |
將現在時間設定成指定的時間並暫停時間,不過是使用「過了多久的秒數(duration)」來設定假時間。 | travel(duration) | freeze(Time.now + duration) |
復原現在的時間 | travel_back | return |
下面會以ActiveSupport::Testing::TimeHelpers的語法為例。
要在測試中使用ActiveSupport::Testing::TimeHelpers提供的method,則必須先在spec/rails_helper.rb裡include TimeHelpers。
spec/rails_helper.rb
RSpec.configure do |config|
# ...
# Setup time helper
config.include ActiveSupport::Testing::TimeHelpers
# ...
end
如果要跳到某個時間點之後暫停時間,則可以使用travel。當測試完時,要記得呼叫travel_back回到原本的時間。如果沒有呼叫travel_back,則假時間會繼續套用在其它的測試案例,這樣可能會造成非預期的測試結果。
spec/models/project_spec.rb
# ...
it "finds recently started projects" do
travel_to(Date.parse("2015-02-10"))
actual = Project.find_recently_started(6.months)
expect(actual.size).to eq(3)
travel_back
end
# ...
如果你想要所有的測試都要設定假時間,那可以在setup時呼叫travel_to或是travel,在teardown時呼叫travel_back。
travel_to與travel都支援後面接block的語法,在block中時間是假的,但離開block後時間就會變正常,travel_back可以不用寫。
spec/models/project_spec.rb
# ...
it "finds recently started projects" do
travel_to(Date.parse("2015-02-10")) do
actual = Project.find_recently_started(6.months)
expect(actual.size).to eq(3)
end
end
# ...
另外假時間可以在測試中的任何時間點使用,例如下面的例子可以達到快轉的效果:
# ...
it "knows if the project is over" do
p = Project.new(:start_date => Date.today, :end_date = Date.today + 8.weeks)
expect(p).not_to be_complete
travel(10.weeks)
expect(p).to be_complete
travel_back
end
# ...