Rails - 测试使用 DateTime.now 的方法

Rails - Testing a method that uses DateTime.now

我有一个使用 DateTime.now 对某些数据执行搜索的方法,我想用各种日期测试该方法,但我不知道如何存根 DateTime.now 也不能让它与 Timecop 一起工作(如果它能像那样工作的话)。

我试过时间警察

it 'has the correct amount if falls in the previous month' do
      t = "25 May".to_datetime
      Timecop.travel(t)
      puts DateTime.now

      expect(@employee.monthly_sales).to eq 150
end 

当我 运行 规范时,我可以看到 puts DateTime.now 给出 2015-05-25T01:00:00+01:00 但在我测试输出的方法中具有相同的 puts DateTime.now 2015-07-24T08:57:53+01:00(今天的日期)。 我怎样才能做到这一点?

----------------更新------------------------ ------------------------

我在 before(:all) 块中设置记录(@employee 等),这似乎导致了问题。它仅在 Timecop do 块之后完成设置时才有效。为什么会这样?

Timecop 应该能够处理您想要的。尝试在 运行 考试之前冻结时间,而不仅仅是旅行,然后在完成后解冻。像这样:

before do
  t = "25 May".to_datetime
  Timecop.freeze(t)
end

after do
  Timecop.return
end

it 'has the correct amount if falls in the previous month' do
  puts DateTime.now
  expect(@employee.monthly_sales).to eq 150
end 

来自 Timecop 的自述文件:

freeze is used to statically mock the concept of now. As your program executes, Time.now will not change unless you make subsequent calls into the Timecop API. travel, on the other hand, computes an offset between what we currently think Time.now is (recall that we support nested traveling) and the time passed in. It uses this offset to simulate the passage of time.

所以你想把时间定格在某个地方,而不是仅仅穿越到那个时间。因为时间会像往常一样随着旅行而流逝,但起点不同。

如果这仍然不起作用,您可以将您的方法调用与 Timecop 放在一个块中,以确保它冻结块内的时间,例如:

t = "25 May".to_datetime
Timecop.travel(t) do # Or use freeze here, depending on what you need
  puts DateTime.now
  expect(@employee.monthly_sales).to eq 150
end

TL;DR:问题是在 Employee 中调用了 DateTime.now,然后在规范中调用了 Timecop.freeze

Timecop 模拟了 TimeDateDateTime 的构造函数。在 freezereturn 之间(或在 freeze 块内)创建的任何实例都将被模拟。
freeze 之前或 return 之后创建的任何实例都不会受到影响,因为 Timecop 不会混淆现有对象。

来自README(我的重点):

A gem providing "time travel" and "time freezing" capabilities, making it dead simple to test time-dependent code. It provides a unified method to mock Time.now, Date.today, and DateTime.now in a single call.

因此,在创建要模拟的 Time 对象之前,必须先调用 Timecop.freeze。如果您在 RSpec before 块中 freeze,则在计算 subject 之前这将是 运行。但是,如果您在设置主题的地方有一个 before 块(在您的情况下为 @employee),并且在嵌套的 describe 中有另一个 before 块,那么您的主题已经设置好,在你冻结时间之前调用了 DateTime.new


如果您将以下内容添加到 Employee

中会发生什么
class Employee
  def now
    DateTime.now
  end
end

然后你运行以下规格:

describe '#now' do
  let(:employee) { @employee }
  it 'has the correct amount if falls in the previous month', focus: true do
    t = "25 May".to_datetime
    Timecop.freeze(t) do
      expect(DateTime.now).to eq t
      expect(employee.now).to eq t

      expect(employee.now.class).to be DateTime
      expect(employee.now.class.object_id).to be DateTime.object_id
    end
  end
end

除了使用 freeze 块,您还可以在 rspec beforeafter 钩子中使用 freezereturn

describe Employee do
  let(:frozen_time) { "25 May".to_datetime }
  before { Timecop.freeze(frozen_time) }
  after { Timecop.return }
  subject { FactoryGirl.create :employee }

  it 'has the correct amount if falls in the previous month' do
    # spec here
  end

end

题外话,但也许可以看看 http://betterspecs.org/

我 运行 遇到了 Timecop 和其他与日期、时间和 DateTime 类 及其方法混淆的神奇东西的几个问题。我发现最好只使用依赖注入:

员工代码

class Employee
  def monthly_sales(for_date = nil)
    for_date ||= DateTime.now

    # now calculate sales for 'for_date', instead of current month
  end
end 

规格

it 'has the correct amount if falls in the previous month' do
  t = "25 May".to_datetime
  expect(@employee.monthly_sales(t)).to eq 150
end 

我们,Ruby 世界的人们,在使用一些魔术技巧时找到了极大的乐趣,而那些使用表达能力较差的编程语言的人却无法利用这些技巧。但这种情况下魔法太黑暗了,真的应该避免。只需使用普遍接受的依赖注入最佳实践方法即可。