如何伪造 Time.now?

Posted

技术标签:

【中文标题】如何伪造 Time.now?【英文标题】:How to fake Time.now? 【发布时间】:2010-11-15 23:02:37 【问题描述】:

为了在单元测试中测试时间敏感的方法,设置Time.now 的最佳方式是什么?

【问题讨论】:

有时间领主红宝石吗? :P 关闭!有时间警察。 (见下面我的回答。) 【参考方案1】:

我自己的解决方案https://github.com/igorkasyanchuk/rails_time_travel - 带有 UI 的 gem,因此您无需在代码中硬编码任何日期时间。只需从 UI 中更改即可。

它可能对您的 QA 团队或在 staging 上测试应用程序也非常有用。

【讨论】:

【参考方案2】:

如果您包含 ActiveSupport,您可以使用:

travel_to Time.zone.parse('2010-07-05 08:00')

http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html

【讨论】:

【参考方案3】:

使用 Rspec 3.2,我发现伪造 Time.now 返回值的唯一简单方法是:

now = Time.parse("1969-07-20 20:17:40")
allow(Time).to receive(:now)  now 

Now Time.now 将始终返回阿波罗 11 号登月的日期。

来源:https://www.relishapp.com/rspec/rspec-mocks/docs

【讨论】:

非常好的答案!这是迄今为止最简单的解决方案,并且静态模拟像时间一样基本的东西似乎是一种合理的方法,因为最终会将所有基础库注入到您的类中。【参考方案4】:

遇到同样的问题,我不得不为特定日期的规范伪造时间,而时间只是这样做了:

Time.stub!(:now).and_return(Time.mktime(2014,10,22,5,35,28))        

这会给你:

2014-10-22 05:35:28 -0700

【讨论】:

感叹号做了什么?这是用 mocha gem 吗? 其实我觉得stub!已经被贬低了,你现在可以直接使用stub【参考方案5】:

执行time-warp

time-warp 是一个可以做你想做的事的库。它为您提供了一个需要时间和块的方法,并且块中发生的任何事情都使用伪造的时间。

pretend_now_is(2000,"jan",1,0) do
  Time.now
end

【讨论】:

【参考方案6】:

最近发布的Test::Redef 使这个和其他伪造变得容易,即使没有以依赖注入风格重构代码(如果您使用其他人的代码特别有用。)

fake_time = Time.at(12345) # ~3:30pm UTC Jan 1 1970
Test::Redef.rd 'Time.now' => proc  fake_time  do
   assert_equal 12345, Time.now.to_i
end

但是,请注意其他获取时间的方法,以免它被伪造(Date.new,一个编译后的扩展,可以进行自己的系统调用,连接到诸如知道当前时间戳的外部数据库服务器之类的东西的接口等)它听起来上面的 Timecop 库可能会克服这些限制。

其他很好的用途包括测试诸如“当我尝试使用这个友好的 http 客户端但它决定引发这个异常而不是返回一个字符串时会发生什么?”没有实际设置导致该异常的网络条件(这可能很棘手)。它还允许您检查重定义函数的参数。

【讨论】:

【参考方案7】:

我真的很喜欢Timecop 库。您可以以块的形式进行时间扭曲(就像时间扭曲一样):

Timecop.travel(6.days.ago) do
  @model = TimeSensitiveMode.new
end
assert @model.times_up!

(是的,您可以嵌套块形式的时间旅行。)

您也可以进行声明式时间旅行:

class MyTest < Test::Unit::TestCase
  def setup
    Timecop.travel(...)
  end
  def teardown
    Timecop.return
  end
end

我有一些 cucumber Timecop here 的助手。他们允许您执行以下操作:

Given it is currently January 24, 2008
And I go to the new post page
And I fill in "title" with "An old post"
And I fill in "body" with "..."
And I press "Submit"
And we jump in our Delorean and return to the present
When I go to the home page
I should not see "An old post"

【讨论】:

最后一次测试的末尾应该加引号吗?【参考方案8】:

不要忘记Time 只是一个引用类对象的常量。如果你愿意引起警告,你总是可以这样做

real_time_class = Time
Time = FakeTimeClass
# run test
Time = real_time_class

【讨论】:

【参考方案9】:

我总是将Time.now 提取到一个单独的方法中,然后在模拟中将其转换为attr_accessor

【讨论】:

【参考方案10】:

我的测试文件中只有这个:

   def time_right_now
      current_time = Time.parse("07/09/10 14:20")
      current_time = convert_time_to_utc(current_date)
      return current_time
    end

在我的 Time_helper.rb 文件中我有一个

  def time_right_now
    current_time= Time.new
    return current_time
  end

所以在测试时 time_right_now 被覆盖以使用您想要的任何时间。

【讨论】:

【参考方案11】:

我正在使用 RSpec,我在调用 Time.now 之前这样做了:Time.stub!(:now).and_return(2.days.ago)。这样我就可以控制用于特定测试用例的时间

【讨论】:

你的测试最终不会改变吗?有什么例子吗? 例如如果我有一个租赁系统,我想测试如果物品归还的那一天晚了,系统是否会收取罚款,我需要操纵 Time.now 以便当物品归还时,它会迟到。 .. 不知道你是否明白我的意思【参考方案12】:

另请参阅this question 我也发表了此评论。

根据您将Time.now 与什么进行比较,有时您可以更改固定装置以实现相同的目标或测试相同的功能。例如,我有一种情况,如果某个日期在未来,我需要发生一件事,如果是在过去,我需要发生另一件事。我能够做的是在我的固定装置中加入一些嵌入式红宝石(erb):

future:
    comparing_date: <%= Time.now + 10.years %>
    ...

past:
    comparing_date: <%= Time.now - 10.years %>
    ...

然后在您的测试中,您可以根据相对于Time.now 的时间选择使用哪一个来测试不同的功能或操作。

【讨论】:

谢谢,我发现这比伪造 Time.now 优雅得多。您的测试还将确保在任何地方都没有硬编码的日期,并且您的代码在您运行它的那天仍然按预期工作【参考方案13】:

根据您将Time.now 与什么进行比较,有时您可以更改固定装置以实现相同的目标或测试相同的功能。例如,我有一种情况,如果某个日期在未来,我需要发生一件事,如果是在过去,我需要发生另一件事。我能够做的是在我的固定装置中加入一些嵌入式红宝石(erb):

future:
    comparing_date: <%= Time.now + 10.years %>
    ...

past:
    comparing_date: <%= Time.now - 10.years %>
    ...

然后在您的测试中,您可以根据相对于Time.now 的时间选择使用哪一个来测试不同的功能或操作。

【讨论】:

【参考方案14】:

这种工作方式允许嵌套:

class Time
  class << self
    attr_accessor :stack, :depth
  end

  def self.warp(time)

    Time.stack ||= [] 
    Time.depth ||= -1 
    Time.depth += 1
    Time.stack.push time

    if Time.depth == 0 
      class << self    
          alias_method :real_now, :now  
          alias_method :real_new, :new

          define_method :now do
            stack[depth] 
          end

          define_method :new do 
            now 
          end
      end 
    end 

    yield

    Time.depth -= 1
    Time.stack.pop 

    class << self 
      if Time.depth < 0 
        alias_method :new, :real_new
        alias_method :now, :real_now
        remove_method :real_new
        remove_method :real_now 
      end
    end

  end
end

可以通过在末尾取消定义堆栈和深度访问器来稍微改进

用法:

time1 = 2.days.ago
time2 = 5.months.ago
Time.warp(time1) do 

  Time.real_now.should_not == Time.now

  Time.now.should == time1 
  Time.warp(time2) do 
    Time.now.should == time2
  end 
  Time.now.should == time1
end

Time.now.should_not == time1 
Time.now.should_not be_nil

【讨论】:

【参考方案15】:

我个人更喜欢使时钟可注入,如下所示:

def hello(clock=Time)
  puts "the time is now: #clock.now"
end

或者:

class MyClass
  attr_writer :clock

  def initialize
    @clock = Time
  end

  def hello
    puts "the time is now: #@clock.now"
  end
end

但是,许多人更喜欢使用模拟/存根库。在 RSpec/flexmock 你可以使用:

Time.stub!(:now).and_return(Time.mktime(1970,1,1))

或者在摩卡中:

Time.stubs(:now).returns(Time.mktime(1970,1,1))

【讨论】:

测试部分正是我要提供的那种答案。 这完全是我会做的。如果您以易于测试的方式编写代码,则无需为了使其可测试而做一些晦涩难懂的事情。 虽然我同意以更可测试的方式编写内容的一般想法,但有时它根本不实用。 Time.now 就是其中的一个例子。它可以在系统的很多部分中使用,并且一直传递它会带来太多开销。

以上是关于如何伪造 Time.now?的主要内容,如果未能解决你的问题,请参考以下文章

如何在日期范围内获取数据?

如何将分钟添加到时间对象

如何计算rails admin中的年龄作为出口值?

如何在 Rails 中将当前时间添加 10 天

如何写没有前导零或前导空格的时间?

golang 如何查看程序执行消耗时间