ChatGPT解决这个技术问题 Extra ChatGPT

无法用 RSpec 比较时间

我正在使用 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,因此来自检查的文本是匹配的,但在下面你有两个不同的 Time 对象。您可以只使用 ===,但这可能会因跨越第二个边界而受到影响。最好的办法可能是找到或编写自己的匹配器,在其中转换为纪元秒并允许存在小的绝对差异。
如果我理解您与“跨越第二个界限”有关,那么问题应该不会出现,因为我使用的是“冻结”时间的 Timecop gem。
啊,我错过了,对不起。在这种情况下,只需使用 === 而不是 == - 目前您正在比较两个不同 Time 对象的 object_id。虽然 Timecop 不会冻结数据库服务器时间。 . .因此,如果您的时间戳是由 RDBMS 生成的,它将不起作用(我希望这对您来说不是问题)

D
Dorian

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

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

.000001 s 有点紧。我什至尝试了.001,但有时会失败。我认为即使是 0.1 秒也可以证明时间已定为现在。对我的目的来说已经足够好了。
我认为这个答案更好,因为它表达了测试的意图,而不是将其隐藏在一些不相关的方法(如 to_s)后面。此外,如果时间接近一秒,to_ito_s 可能很少失败。
看起来 be_within 匹配器是在 2010 年 11 月 7 日添加到 RSpec 2.1.0 中的,并且从那时起经过了几次增强。 rspec.info/documentation/2.14/rspec-expectations/…
我得到undefined method 'second' for 1:Fixnum。我有什么需要require的吗?
@DavidMoles .second 是 Rails 扩展:api.rubyonrails.org/classes/Numeric.html
C
Community

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)

请参阅 this 了解有关时间不同原因的更多信息


正如您在问题代码中看到的那样,我使用了 Timecop gem。它应该通过“冻结”时间来解决问题吗?
您将时间保存在数据库中并检索它(@article.updated_at),这使其松散了纳秒,而 Time.now 保留了纳秒。从我回答的前几行应该很清楚
接受的答案比下面的答案早近一年 - be_within 匹配器是处理这个问题的更好方法:A)你不需要宝石; B) 它适用于任何类型的值(整数、浮点数、日期、时间等); C) 它本身就是 RSpec 的一部分
这是一个“OK”的解决方案,但绝对 be_within 是正确的
您好我正在使用日期时间比较与作业排队延迟预期expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time)我不确定.at匹配器是否会接受转换为整数的时间.to_i有人遇到过这个问题吗?
j
jBilbo

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

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 或担心小数。


确保在规范结束时解冻时间
将其更改为使用块。这样你就不会忘记回到正常时间。
e
equivalent8

是的,因为 Oin 建议 be_within matcher 是最佳做法

...而且它还有更多用例 -> 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

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

所以是的,请坚持使用 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 匹配器(例如,您甚至可以执行 API testing with pure RSpec

至于“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)

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

如果时间在哈希中怎么办?当某些 hash_1 值是 pre-db-times 并且 hash_2 中的相应值是 post-db-times 时,有什么方法可以使 expect(hash_1).to eq(hash_2) 工作?
我在考虑任意散列(我的规范断言操作根本不会更改记录)。不过谢谢!
S
Sam

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

.change(usec: 0) 很有帮助
我们可以使用这个 .change(usec: 0) 技巧来解决问题,而无需使用规范助手。如果第一行是 Timecop.freeze(Time.current.change(usec: 0)),那么我们可以简单地比较最后的 .to eq(Time.now)
T
Thomas Klemm

您可以使用 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)

Q
Qwertie

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

如果 updated_at 对您的用例没有用处,您可以寻求更优雅的解决方案,而不是将其与自身进行比较:expect(data).to match(hash_including({ some_other_keys: ... }))
C
Chris Edwards

在 Rails 4.1+ 中,您可以使用 Time Helpers

include ActiveSupport::Testing::TimeHelpers

describe "some test" do
  around { |example| freeze_time { example.run } }

  it "updates updated_at attribute" do
    expect { patch :update }.to change { @article.reload.updated_at }.to(Time.current)
  end
end