我正在使用 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 ==)
===
,但这可能会因跨越第二个边界而受到影响。最好的办法可能是找到或编写自己的匹配器,在其中转换为纪元秒并允许存在小的绝对差异。
===
而不是 ==
- 目前您正在比较两个不同 Time 对象的 object_id。虽然 Timecop 不会冻结数据库服务器时间。 . .因此,如果您的时间戳是由 RDBMS 生成的,它将不起作用(我希望这对您来说不是问题)
我发现使用 be_within
默认 rspec 匹配器更优雅:
expect(@article.updated_at.utc).to be_within(1.second).of Time.now
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。它应该通过“冻结”时间来解决问题吗?
be_within
是正确的
expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time)
我不确定.at
匹配器是否会接受转换为整数的时间.to_i
有人遇到过这个问题吗?
旧帖子,但我希望它可以帮助任何进入这里寻求解决方案的人。我认为手动创建日期更容易、更可靠:
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
或担心小数。
是的,因为 Oin
建议 be_within
matcher 是最佳做法
...而且它还有更多用例 -> 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
匹配器!
所以是的,请坚持使用 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.
expect(hash_1).to eq(hash_2)
工作?
我发现解决此问题的最简单方法是创建一个 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)
。
您可以使用 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)
因为我在比较哈希,所以这些解决方案中的大多数对我都不起作用,所以我发现最简单的解决方案是简单地从我正在比较的哈希中获取数据。由于 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: ... }))
在 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
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
的吗?.second
是 Rails 扩展:api.rubyonrails.org/classes/Numeric.html