【问题标题】:Rails Time inconsistencies with rspecRails 时间与 rspec 不一致
【发布时间】:2011-04-03 08:40:20
【问题描述】:

我正在使用 Time in Rails 并使用以下代码设置项目的开始日期和结束日期:

start_date ||= Time.now
end_date = start_date + goal_months.months

然后我克隆对象并编写 rspec 测试以确认副本中的属性匹配。 结束日期匹配:

original[end_date]:  2011-08-24 18:24:53 UTC
clone[end_date]:     2011-08-24 18:24:53 UTC

但是规范在开始日期上给了我一个错误:

expected: Wed Aug 24 18:24:53 UTC 2011,
     got: Wed, 24 Aug 2011 18:24:53 UTC +00:00 (using ==)

很明显,日期是相同的,只是格式不同。它们最终如何以不同的方式存储在数据库中,我如何让它们匹配?我也用 DateTime 进行了尝试,结果相同。

更正:结束日期也不匹配。它们打印出相同的内容,但也会出现 rspec 错误。当我打印出开始日期和结束日期时,值以不同的格式出现:

start date: 2010-08-24T19:00:24+00:00
end date: 2011-08-24 19:00:24 UTC

【问题讨论】:

    标签: ruby-on-rails time rspec


    【解决方案1】:

    这通常是因为 rspec 尝试匹配不同的对象:例如 Time 和 DateTime。此外,可比较的时间可能会略有不同,相差几毫秒。

    在第二种情况下,正确的方法是使用 stubbing 和 mock。另见TimeCop gem

    在第一种情况下,可能的解决方案是比较时间戳:

    actual_time.to_i.should == expected_time.to_i
    

    对于这种情况,我使用简单的matcher

    # ./spec/spec_helper.rb
    #
    # Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
    #
    #
    # Usage:
    #
    # its(:updated_at) { should be_the_same_time_as updated_at }
    #
    #
    # Will pass or fail with message like:
    #
    # Failure/Error: its(:updated_at) { should be_the_same_time_as 2.days.ago }
    # expected Tue, 07 Jun 2011 16:14:09 +0300 to be the same time as Mon, 06 Jun 2011 13:14:09 UTC +00:00
    
    
    RSpec::Matchers.define :be_the_same_time_as do |expected|
      match do |actual|
        expected.to_i == actual.to_i
      end
    end
    

    【讨论】:

    • 我在使用Timecop 时也遇到了这个问题,这真的很奇怪。想法?
    • 原来这只是在我们的持续集成环境中失败了。似乎这是一个 UTC 与非 UTC 的问题,所以我只是将我的规范更改为 order.updated_at.utc.should == Time.now.utc,这应该可以解决它。
    • 我的修复似乎不起作用。我们的 CI 环境仍然说: Diff:2012-11-19 23:54:24 UTC.==(2012-11-19 23:54:24 UTC) 返回 false 即使 2012-11-19 23:54 之间的差异:24 UTC 和 2012-11-19 23:54:24 UTC 为空。检查 2012-11-19 23:54:24 UTC 的执行情况。==.
    • 看起来我会像大家建议的那样使用to_i。 :) blog.tddium.com/2011/08/07/…
    • 与这个匹配器一起,我们最终使用#beginning_of_xxx 来“舍入”Timecop 冻结之前的时间。例如,now = Time.now.beginning_of_hour 后跟 Timecop.freeze(now) do…end
    【解决方案2】:

    您应该模拟 Time 的 now 方法,以确保它始终与规范中的日期匹配。您永远不知道延迟何时会因为几毫秒而使规范失败。这种方法还将确保真实代码和规范上的时间相同。

    如果您使用的是默认的 rspec 模拟库,请尝试执行以下操作:

    t = Time.parse("01/01/2010 10:00")
    Time.should_receive(:now).and_return(t)
    

    【讨论】:

      【解决方案3】:

      我完全同意之前关于存根 Time.now 的答案。也就是说,这里还有另一件事。当您比较数据库中的日期时间时,您会丢失一些可能在 ruby​​ DateTime obj 中的派系时间。在 Rspec 中以这种方式比较日期的最佳方法是:

      database_start_date.should be_within(1).of(start_date)
      

      【讨论】:

        【解决方案4】:

        一个问题是 Ruby Time 对象具有纳秒精度,但大多数数据库最多具有微秒精度。解决此问题的最佳方法是使用整数存根 Time.now(或使用 timecop)。阅读我写的关于这个的帖子:http://blog.solanolabs.com/rails-time-comparisons-devil-details-etc/

        【讨论】:

        【解决方案5】:

        我最初的猜测是 Time.now 的值与您的数据库值的格式不同。

        【讨论】:

          【解决方案6】:

          您确定您使用的是== 而不是eqlbe?后两种方法使用对象标识而不是比较值。

          从输出看来,预期值为Time,而正在测试的值为DateTime。这也可能是一个问题,尽管鉴于 Ruby 的日期和时间库几乎病态的性质,我不敢猜测如何解决它......

          【讨论】:

          • 使用 DateTime 而不是 Time 时,你得到相同的输出吗?
          【解决方案7】:

          我喜欢的一个解决方案是将以下内容添加到spec_helper

          class Time
            def ==(time)
              self.to_i == time.to_i
            end
          end
          

          这样即使在嵌套对象中也是完全透明的。

          【讨论】:

          • 仅在测试中更改核心类的行为似乎是个坏主意。
          • 我实际上也遇到了一些问题,因为 Rails 中出现了各种类似 Time 的类。我可能会开始将 Timecop 集成到我的项目中,尽管我怀疑他们会做类似的事情。
          【解决方案8】:

          .to_datetime 添加到两个变量将强制日期时间值相等并尊重时区和夏令时。对于日期比较,请使用.to_date

          具有两个变量的示例规范: actual_time.to_datetime.should == expected_time.to_datetime

          更清晰的规范: actual_time.to_datetime.should eq 1.month.from_now.to_datetime

          .to_i 在规范中产生歧义。

          +1 用于在规范中使用 TimeCop gem。如果您的应用受 DST 影响,请务必在规范中测试夏令时。

          【讨论】:

            【解决方案9】:

            我们当前的解决方案是有一个处理舍入的freeze_time 方法:

            def freeze_time(time = Time.zone.now)
              # round time to get rid of nanosecond discrepancies between ruby time and
              # postgres time
              time = time.round
              Timecop.freeze(time) { yield(time) }
            end
            

            然后你可以像这样使用它:

            freeze_time do
              perform_work
              expect(message.reload.sent_date).to eq(Time.now)
            end
            

            【讨论】:

              【解决方案10】:

              根据您的规格,您也许可以使用 Rails-native travel helpers

              # in spec_helper.rb
              config.include ActiveSupport::Testing::TimeHelpers
              
              start_date ||= Time.current.change(usecs: 0)
              end_date = start_date + goal_months.months
              travel_to start_date do
                # Clone here  
              end
              expect(clone.start_date).to eq(start_date)
              

              如果没有Time.current.change(usecs: 0),它可能会抱怨时区之间的差异。或者在微秒之间,因为助手会在内部重置传递的值(Timecop 也有类似的问题,所以也用它重置usecs)。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2015-01-27
                • 1970-01-01
                • 2017-12-17
                • 2012-04-12
                相关资源
                最近更新 更多