与 RSpec 的时间比较有问题

我使用的是 Ruby on Rails4和 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

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

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 ==)
73780 次浏览

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

如果您不关心毫秒差,那么您可以在期望的两端执行 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)

有关时间不同的更多信息,请参考 这个

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

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

老职位,但我希望它能帮助任何人谁进入这里解决方案。我认为手动创建日期更容易也更可靠:

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

可以使用 to_s(:db)将 date/datetime/time 对象转换为字符串,因为它存储在数据库中。

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)

是的,因为 Oin是建议 be_within匹配是最好的做法

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

但是处理这个问题的另一种方法是使用内置在 middaymiddnight属性中的 Rails。

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 call = > all time 属性将会有相同的 time = > 可能不能证明你正在尝试实现的概念。我通常在组合 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 的时候!

所以是请坚持与 be_within匹配;)


2017-02年度最新情况

评论中的问题:

如果时间是在散列中呢?使期望(hash _ 1)的任何方法。当一些 hash _ 1值是前 db 时间,而 hash _ 2中的相应值是后 db 时间时,如何使用 eq (hash _ 2) ?-

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

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

至于“ post-DB-times”,我猜你指的是保存到 DB 后生成的字符串。我建议将这种情况解耦为两个期望(一个确保哈希结构,第二个检查时间) ,因此您可以这样做:

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.

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

因为我在比较散列,所以这些解决方案中的大多数对我来说都不起作用,所以我发现最简单的解决方案就是从我正在比较的散列中获取数据。由于 update _ at 时间对于我来说实际上并没有什么用处,所以我可以很好地测试它。

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


expect(data).to eq(
{updated_at: data[:updated_at], some_other_keys: ...}
)

在 Rails 4.1 + 中,你可以使用 时间助手:

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