无法将时间与RSpec进行比较


119

我正在使用Ruby on Rails 4和rspec-rails gem 2.14。对于我的对象,我想updated_at在控制器操作运行后将当前时间与对象属性进行比较,但是由于规格未通过,我感到很麻烦。也就是说,给出以下是规范代码:

it "updates updated_at attribute" do
  Timecop.freeze

  patch :update
  @article.reload
  expect(@article.updated_at).to eq(Time.now)
end

当我运行以上规范时,出现以下错误:

Failure/Error: expect(@article.updated_at).to eq(Time.now)

   expected: 2013-12-05 14:42:20 UTC
        got: Thu, 05 Dec 2013 08:42:20 CST -06:00

   (compared using ==)

如何使规格通过?


注意:我也尝试了以下操作(请注意utc添加内容):

it "updates updated_at attribute" do
  Timecop.freeze

  patch :update
  @article.reload
  expect(@article.updated_at.utc).to eq(Time.now)
end

但规格仍未通过(请注意“获得”的价值差异):

Failure/Error: expect(@article.updated_at.utc).to eq(Time.now)

   expected: 2013-12-05 14:42:20 UTC
        got: 2013-12-05 14:42:20 UTC

   (compared using ==)

它正在比较对象ID,因此来自inspect的文本是匹配的,但是在下面有两个不同的Time对象。您可以只使用===,但是可能会遇到跨越第二个边界的问题。最好是找到或编写自己的匹配器,在该匹配器中,您将转换为新纪元,并允许一个很小的绝对差。
尼尔·斯莱特2013年

如果我理解您与“跨越第二界线”有关,那么应该不会出现此问题,因为我使用的是“冻结”时间的Timecop宝石。
Backo 2013年

啊,我很想念,对不起。在这种情况下,请使用===代替==-当前正在比较两个不同的Time对象的object_id。尽管Timecop不会冻结数据库服务器的时间。。。因此,如果您的时间戳是由RDBMS生成的,则它将不起作用(我希望这对您来说不是问题)
Neil Slater 2013年

Answers:


156

Ruby Time对象比数据库保持更高的精度。从数据库读回该值时,它只会保留到微秒级的精度,而内存中的表示则精确到纳秒。

如果您不关心毫秒差,则可以在期望的两侧进行to_s / to_i

expect(@article.updated_at.utc.to_s).to eq(Time.now.to_s)

要么

expect(@article.updated_at.utc.to_i).to eq(Time.now.to_i)

有关更多信息,请参阅内容


如您在问题代码中所见,我使用了Timecopgem。它应该通过“冻结”时间来解决问题吗?
Backo 2013年

您将时间保存在数据库中并进行检索(@ article.updated_at),这使它失去了纳秒级,而Time.now保留了纳秒级。从我的回答的前几行中应该很清楚
usha 2013年

3
可接受的答案比下面的答案大了将近一年-be_within匹配器是处理此问题的更好方法:A)您不需要宝石;B)它适用于任何类型的值(整数,浮点数,日期,时间等);C)它本身是RSpec的一部分
notaceo 2015年

3
这是一个“确定”的解决方案,但绝对be_within是正确的解决方案
Francesco Belladonna

您好,我正在将日期时间比较与作业入队延迟期望一起使用expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) 我不确定.at匹配器是否会接受与.to_i任何人都遇到过此问题的转换为整数的时间?
西里尔·杜尚-多丽丝

200

我发现使用be_within默认的rspec匹配器更加优雅:

expect(@article.updated_at.utc).to be_within(1.second).of Time.now

2
.000001 s有点紧。我什至尝试了.001,但有时却失败了。我认为即使是.1秒也可以证明时间已经到现在了。对于我的目的来说足够好了。
2014年

3
我认为这个答案更好,因为它表达了测试的意图,而不是在诸如的不相关方法后面使测试难以理解to_s。此外,to_ito_s可能,如果时间是附近的第二年底很少失败。
B 2014年

2
看起来该be_within匹配器已于2010年11月7日添加到RSpec 2.1.0,并且此后进行了几次增强。rspec.info/documentation/2.14/rspec-expectations/...
马克·贝瑞

1
我懂了undefined method 'second' for 1:Fixnum。我有什么需要require吗?
大卫·摩尔

3
@DavidMoles .second是rails扩展名:api.rubyonrails.org/classes/Numeric.html
jwadsa​​ck,2015年

10

是,因为Oin这表明be_within匹配器是最佳做法

...还有更多用例-> http://www.eq8.eu/blogs/27-rspec-be_within-matcher

但是,解决该问题的另一种方法是使用内置的Rails middaymiddnight属性。

it do
  # ...
  stubtime = Time.now.midday
  expect(Time).to receive(:now).and_return(stubtime)

  patch :update 
  expect(@article.reload.updated_at).to eq(stubtime)
  # ...
end

现在,这只是为了演示!

我不会在控制器中使用此方法,因为您将对所有Time.new调用进行存根=>所有时间属性将具有相同的时间=>可能无法证明您正在尝试实现的概念。我通常在类似于以下的组合Ruby对象中使用它:

class MyService
  attr_reader :time_evaluator, resource

  def initialize(resource:, time_evaluator: ->{Time.now})
    @time_evaluator = time_evaluator
    @resource = resource
  end

  def call
    # do some complex logic
    resource.published_at = time_evaluator.call
  end
end

require 'rspec'
require 'active_support/time'
require 'ostruct'

RSpec.describe MyService do
  let(:service) { described_class.new(resource: resource, time_evaluator: -> { Time.now.midday } ) }
  let(:resource) { OpenStruct.new }

  it do
    service.call
    expect(resource.published_at).to eq(Time.now.midday)    
  end
end

但老实说,be_within即使在比较Time.now.midday时,我也建议坚持使用匹配器!

是的,请坚持用be_within匹配器;)


更新2017-02

评论中的问题:

如果时间在哈希中怎么办?当某些hash_1值为pre-db-times并且hash_2中对应的值为post-db-times时,有什么方法可以使Expect(hash_1).to eq(hash_2)工作?–

expect({mytime: Time.now}).to match({mytime: be_within(3.seconds).of(Time.now)}) `

您可以将任何RSpec匹配器传递给match匹配器(例如,甚至可以使用纯RSpec进行API测试

至于“ post-db-times”,我想你的意思是保存到数据库后生成的字符串。我建议将这种情况与2个期望脱钩(一个确保哈希结构,第二个检查时间),所以您可以执行以下操作:

hash = {mytime: Time.now.to_s(:db)}
expect(hash).to match({mytime: be_kind_of(String))
expect(Time.parse(hash.fetch(:mytime))).to be_within(3.seconds).of(Time.now)

但是,如果这种情况在您的测试套件中过于常见,我建议编写您自己的RSpec匹配器(例如be_near_time_now_db_string),将db字符串时间转换为Time对象,然后将其用作:的一部分match(hash)

 expect(hash).to match({mytime: be_near_time_now_db_string})  # you need to write your own matcher for this to work.

如果时间在哈希中怎么办?expect(hash_1).to eq(hash_2)当某些hash_1值为pre-db-times并且hash_2中的对应值是post-db-times时,有什么办法可以工作?
Michael Johnston

我在想一个任意的哈希(我的规范断言一个操作根本不会更改记录)。但是谢谢!
Michael Johnston

10

旧帖子,但我希望它对任何进入此处寻求解决方案的人有所帮助。我认为手动创建日期更容易,更可靠:

it "updates updated_at attribute" do
  freezed_time = Time.utc(2015, 1, 1, 12, 0, 0) #Put here any time you want
  Timecop.freeze(freezed_time) do
    patch :update
    @article.reload
    expect(@article.updated_at).to eq(freezed_time)
  end
end

这样可以确保存储的日期是正确的日期,而无需to_x担心或担心小数。


确保在规范结束时解冻时间
Qwertie

更改为使用块代替。这样,您就不会忘记返回正常时间。
jBilbo

8

您可以使用将日期/日期时间/时间对象存储在数据库中,将其转换为字符串to_s(:db)

expect(@article.updated_at.to_s(:db)).to eq '2015-01-01 00:00:00'
expect(@article.updated_at.to_s(:db)).to eq Time.current.to_s(:db)

7

我发现此问题的最简单方法是创建一个current_time测试帮助程序方法,如下所示:

module SpecHelpers
  # Database time rounds to the nearest millisecond, so for comparison its
  # easiest to use this method instead
  def current_time
    Time.zone.now.change(usec: 0)
  end
end

RSpec.configure do |config|
  config.include SpecHelpers
end

现在,时间总是四舍五入到最接近的毫秒,比较起来很简单:

it "updates updated_at attribute" do
  Timecop.freeze(current_time)

  patch :update
  @article.reload
  expect(@article.updated_at).to eq(current_time)
end

1
.change(usec: 0)非常有帮助
科恩。

2
我们也可以使用此.change(usec: 0)技巧来解决问题,而无需使用任何规范助手。如果第一行是,Timecop.freeze(Time.current.change(usec: 0))那么我们可以简单地.to eq(Time.now)在末尾进行比较。
哈里·伍德

0

因为我在比较散列,所以大多数解决方案都不适合我,所以我发现最简单的解决方案是简单地从比较中的哈希值中获取数据。由于updated_at时间实际上对我来说没有用,因此无法正常测试。

data = { updated_at: Date.new(2019, 1, 1,), some_other_keys: ...}

expect(data).to eq(
  {updated_at: data[:updated_at], some_other_keys: ...}
)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.