19

我有一个使用 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块之后完成设置时才有效。为什么会这样?

4

3 回答 3

14

TL; DR:问题是在规范中DateTime.now调用Employee之前调用过。Timecop.freeze

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

自述文件(我的重点):

一个提供“时间旅行”和“时间冻结”功能的 gem,使得测试时间相关代码变得非常简单。它提供了一个统一的方法来在一次调用中模拟 Time.now、Date.today 和 DateTime.now。

因此,在创建要模拟Timecop.freeze的对象之前调用是必不可少的。Time如果您freeze在 RSpecbefore块中,这将在subject评估之前运行。但是,如果您有一个before设置主题的块(在您的情况下),并且在嵌套中@employee有另一个块,那么您的主题已经设置,在您冻结时间之前已经调用。beforedescribeDateTime.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块之外,您还可以在rspecfreeze和钩子中使用:returnbeforeafter

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/

于 2015-07-28T16:07:49.757 回答
3

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 用于静态模拟 now 的概念。当您的程序执行时,除非您对 Timecop API 进行后续调用,否则 Time.now 不会更改。另一方面,travel 计算我们当前认为的 Time.now (回想一下我们支持嵌套旅行)和传入的时间之间的偏移量。它使用这个偏移量来模拟时间的流逝。

因此,您想将时间冻结在某个地方,而不仅仅是前往那个时间。因为时间会像往常一样随着旅行而流逝,但起点不同。

如果这仍然不起作用,您可以使用 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
于 2015-07-27T16:13:53.160 回答
1

我遇到了几个与 Timecop 和其他与 Date、Time 和 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 世界的人们,我们非常喜欢使用一些魔术技巧,而那些使用表达能力较差的编程语言的人无法使用这些技巧。但这就是魔法太黑暗的情况,应该真正避免。只需使用普遍接受的依赖注入最佳实践方法。

于 2015-08-02T18:04:32.977 回答