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

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

【问题讨论】:

  • 有时间领主红宝石吗? :P
  • 关闭!有时间警察。 (见下面我的回答。)

标签: ruby unit-testing time


【解决方案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 就是其中的一个例子。它可以在系统的很多部分中使用,并且一直传递它会带来太多开销。
                    猜你喜欢
                    • 1970-01-01
                    • 2012-09-20
                    • 1970-01-01
                    • 2011-07-13
                    • 2016-11-12
                    • 2012-07-08
                    • 2012-06-02
                    • 2011-07-02
                    • 2011-04-30
                    相关资源
                    最近更新 更多