我有一个使用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
当我运行规范时,我可以看到将DateTime.now置于2015-05-25T01:00:00+01:00
,但是在我正在测试输出2015-07-24T08:57:53+01:00
(今天的日期)的方法中使用相同的DateTime.now。
我怎么能做到这一点?
------------------更新---------------------------- -----------------------
我正在before(:all)
块中设置记录(@employee等),这似乎导致了问题。它仅在Timecop do
块之后完成设置时才有效。为什么会这样?
答案 0 :(得分:11)
TL;DR: The problem was that DateTime.now
was called in Employee
before Timecop.freeze
was called in the specs.
Timecop mocks the constructor of Time
, Date
and DateTime
. Any instance created between freeze
and return
(or inside a freeze
block) will be mocked.
Any instance created before freeze
or after return
won't be affected because Timecop doesn't mess with existing objects.
From the README (my emphasis):
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.
So it is essential to call Timecop.freeze
before you create the Time
object you want to mock. If you freeze
in an RSpec before
block, this will be run before subject
is evaluated. However, if you have a before
block where you set up your subject (@employee
in your case), and have another before
block in a nested describe
, then your subject is already set up, having called DateTime.new
before you froze time.
What happens if you add the following to your Employee
class Employee
def now
DateTime.now
end
end
Then you run the following spec:
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
Instead of using a freeze
block, you can also freeze
and return
in rspec before
and after
hooks:
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
Off-topic, but maybe have a look at http://betterspecs.org/
答案 1 :(得分: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的自述文件:
冻结用于静态嘲笑现在的概念。当您的程序执行时,除非您对Timecop API进行后续调用,否则Time.now不会更改。另一方面,旅行计算我们当前认为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
答案 2 :(得分:0)
我在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世界的人们,很高兴使用一些魔术技巧,那些使用表现力较低的编程语言的人无法使用。但是这就是魔法太暗而应该避免的情况。只需使用普遍接受的依赖注入最佳实践方法。