无法用 RSpec 比较时间
Posted
技术标签:
【中文标题】无法用 RSpec 比较时间【英文标题】:Trouble comparing time with RSpec 【发布时间】:2013-12-22 13:47:09 【问题描述】:我正在使用 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 生成的,它将无法工作(我希望这对您来说不是问题)
【参考方案1】:
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
matcher 是否会接受转换为整数的时间.to_i
任何人都遇到过这个问题?
【参考方案2】:
我发现使用 be_within
默认 rspec 匹配器更优雅:
expect(@article.updated_at.utc).to be_within(1.second).of Time.now
【讨论】:
.000001 s 有点紧。我什至尝试了.001,但有时会失败。我认为即使是 0.1 秒也可以证明时间已定为现在。对我的目的来说已经足够了。 我认为这个答案更好,因为它表达了测试的意图,而不是在to_s
等一些不相关的方法后面掩盖它。此外,如果时间接近一秒,to_i
和 to_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【参考方案3】:
旧帖子,但我希望它可以帮助任何进入这里寻求解决方案的人。我认为手动创建日期更容易、更可靠:
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
或担心小数。
【讨论】:
确保在规范结束时解冻时间 改为使用块。这样你就不会忘记回到正常时间。【参考方案4】:您可以将日期/日期时间/时间对象转换为字符串,因为它使用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)
【讨论】:
【参考方案5】:是的,Oin
建议 be_within
匹配器是最佳实践
...它还有更多用例 -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher
但另一种处理方法是使用 Rails 内置的 midday
和 middnight
属性。
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
matcher!
所以是的,请坚持使用be_within
matcher ;)
2017 年 2 月更新
评论中的问题:
如果时间在哈希中怎么办?当一些 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)
工作?
我在考虑任意散列(我的规范断言操作根本不会更改记录)。不过谢谢!【参考方案6】:
我发现解决此问题的最简单方法是创建一个 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)
。【参考方案7】:
因为我在比较哈希,所以这些解决方案中的大多数对我都不起作用,所以我发现最简单的解决方案是简单地从我正在比较的哈希中获取数据。由于 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: ... ))
以上是关于无法用 RSpec 比较时间的主要内容,如果未能解决你的问题,请参考以下文章
Rspec:“array.should == another_array”但不关心顺序
我无法使用 'bundle exec rspec' 执行 rspec
rails-rspec 错误无法加载此类文件 -- rspec/core/formatters/progress_formatter