Ruby-on-rails 无法将时间与 RSpec 进行比较

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/20403063/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 23:06:47  来源:igfitidea点击:

Trouble comparing time with RSpec

ruby-on-railsrubytimerspeccomparison

提问by Backo

I am using Ruby on Rails 4 and the rspec-rails gem 2.14. For a my object I would like to compare the current time with the updated_atobject attribute after a controller action run, but I am in trouble since the spec does not pass. That is, given the following is the spec code:

我正在使用 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

When I run the above spec I get the following error:

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

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 ==)

How can I make the spec to pass?

我怎样才能使规范通过?



Note: I tried also the following (note the utcaddition):

注意:我也尝试了以下(注意utc添加):

it "updates updated_at attribute" do
  Timecop.freeze

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

but the spec still does not pass (note the "got" value difference):

但规范仍然没有通过(注意“得到”的价值差异):

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 ==)

回答by Oin

I find using the be_withindefault rspec matcher more elegant:

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

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

回答by usha

Ruby Time object maintains greater precision than the database does. When the value is read back from the database, it's only preserved to microsecond precision, while the in-memory representation is precise to nanoseconds.

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

If you don't care about millisecond difference, you could do a to_s/to_i on both sides of your expectation

如果你不关心毫秒差异,你可以在你期望的两边做一个 to_s/to_i

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

or

或者

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

Refer to thisfor more information about why the times are different

有关为什么时间不同的更多信息,请参阅

回答by equivalent8

yep as Oinis suggesting be_withinmatcher is the best practice

是的,因为Oin建议be_within匹配器是最佳实践

...and it has some more uscases -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher

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

But one more way how to deal with this is to use Rails built in middayand middnightattributes.

但是另一种处理这个问题的方法是使用 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

Now this is just for demonstration !

现在这只是为了演示!

I wouldn't use this in a controller as you are stubbing all Time.new calls => all time attributes will have same time => may not prove concept you are trying to achive. I usually use it in composed Ruby Objects similar to this:

我不会在控制器中使用它,因为您正在对所有 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

But honestly I recommend to stick with be_withinmatcher even when comparing Time.now.midday !

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

So yes pls stick with be_withinmatcher ;)

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



update 2017-02

更新 2017-02

Question in comment:

评论中的问题:

what if the times are in a Hash? any way to make expect(hash_1).to eq(hash_2) work when some hash_1 values are pre-db-times and the corresponding values in hash_2 are post-db-times? –

如果时间在哈希中怎么办?当某些 hash_1 值是 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)}) `

you can pass any RSpec matcher to the matchmatcher (so e.g. you can even do API testing with pure RSpec)

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

As for "post-db-times" I guess you mean string that is generated after saving to DB. I would suggest decouple this case to 2 expectations (one ensuring hash structure, second checking the time) So you can do something like:

至于“post-db-times”,我猜你的意思是保存到数据库后生成的字符串。我建议将这种情况与两个期望分开(一个确保散列结构,第二个检查时间)所以你可以做这样的事情:

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)

But if this case is too often in your test suite I would suggest writing your own RSpec matcher(e.g. be_near_time_now_db_string) converting db string time to Time object and then use this as a part of the match(hash):

但是,如果这种情况在您的测试套件中过于频繁,我建议您编写自己的 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.

回答by jBilbo

Old post, but I hope it helps anyone who enters here for a solution. I think it's easier and more reliable to just create the date manually:

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

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

This ensures the stored date is the right one, without doing to_xor worrying about decimals.

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

回答by Thomas Klemm

You can convert the date/datetime/time object to a string as it's stored in the database with to_s(:db).

您可以将日期/日期时间/时间对象转换为字符串,因为它使用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)

回答by Sam

The easiest way I found around this problem is to create a current_timetest helper method like so:

我发现解决此问题的最简单方法是创建一个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

Now the time is always rounded to the nearest millisecond to comparisons are straightforward:

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

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

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

回答by Qwertie

Because I was comparing hashes, most of these solutions did not work for me so I found the easiest solution was to simply grab the data from the hash I was comparing. Since the updated_at times are not actually useful for me to test this works fine.

因为我在比较散列,大多数这些解决方案对我不起作用,所以我发现最简单的解决方案是简单地从我比较的散列中获取数据。由于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: ...}
)