RSpecと時間を比較する際の問題


119

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を比較しているため、inspectのテキストは一致していますが、その下には2つの異なるTimeオブジェクトがあります。だけを使用することもできますが===、2番目の境界を超えると問題が発生する可能性があります。おそらく最良の方法は、独自のマッチャーを見つけるか記述することです。この場合、エポック秒に変換し、小さな絶対差を許容します。
Neil Slater、

「第2の境界を越える」ことについて理解していれば、時間を「フリーズ」するTimecop gemを使用しているため、問題は発生しません。
Backo

ああ、すみません。その場合は、===代わりにを使用し==てください。現在、2つの異なるTimeオブジェクトのobject_idを比較しています。Timecopはデータベースサーバーの時間を凍結しませんが。。。したがって、タイムスタンプがRDBMSによって生成されている場合は機能しません(ただし、ここでは問題ではないと思います)
Neil Slater

回答:


156

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)

時間が異なる理由の詳細については、これを参照しください


質問のコードでわかるように、私はTimecop宝石を使用しています。時間を「凍結」するだけで問題を解決できますか?
Backo

時間をデータベースに保存し、それを取得して(@ article.updated_at)、ナノ秒が失われますが、Time.nowはナノ秒を保持しています。それは私の回答の最初の数行から明らかなはずです
usha

3
受け入れられた回答は、以下の回答よりも1年近く古いです。これを処理するには、be_withinマッチャーの方がはるかに優れています。A)宝石は必要ありません。B)あらゆるタイプの値(整数、浮動小数点数、日付、時刻など)で機能します。C)それは本来RSpecの一部です
notaceo

3
これは、「OK」ソリューションですが、間違いなくbe_within正しいものはある
フランチェスコベラドンナ

こんにちは私はジョブのエンキュー遅延の期待値との日時比較を使用しexpect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) ています私は誰かがこの問題に直面.atしたときにマッチャーが整数に変換された時間を受け入れるかどうかわかりませんか.to_i
Cyril Duchon-Doris 2017

199

be_withinデフォルトのrspecマッチャーを使用するとよりエレガントになります。

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

2
.000001 sは少しきついです。.001でも試したところ、失敗することがありました。.1秒でも、時間が現在に設定されていることを証明します。私の目的には十分です。
スモイス2014年

3
この回答は、のようないくつかの無関係なメソッドの背後にあることを隠すのではなく、テストの意図を表すため、より良いと思いますto_s。また、to_iそしてto_s時間が第2の端部の近くにある場合、まれに失敗することがあります。
Bセブン

2
以下のように見えるbe_withinマッチャーは2010年11月7日にRSpecの2.1.0に追加され、それ以来、数回に増強されました。rspec.info/documentation/2.14/rspec-expectations/...
マーク・ベリー

1
わかりますundefined method 'second' for 1:Fixnum。何か必要なことはありrequireますか?
David Moles

3
@DavidMoles .secondはレール拡張です:api.rubyonrails.org/classes/Numeric.html
jwadsa​​ck

10

はい、マッチャーがベストプラクティスであるOinことを示唆しbe_withinています

...そしていくつかのuscasesがあります-> http://www.eq8.eu/blogs/27-rspec-be_within-matcher

しかし、これに対処するもう1つの方法は、組み込みの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

しかし、正直なところ、be_withinTime.now.middayを比較する場合でも、マッチャーを使用することをお勧めします。

だからはいplsはbe_withinマッチャーに固執します;)


アップデート2017-02

コメントの質問:

時間がハッシュにある場合はどうなりますか?一部のhash_1値がpre-db-timeで、hash_2内の対応する値がpost-db-timeである場合にexpect(hash_1).to 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に保存した後に生成される文字列を意味していると思います。このケースを2つの期待に切り離すことをお勧めします(1つはハッシュ構造を確認し、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)

ただし、このケースがテストスイートで頻繁に発生する場合は、独自の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.

時間がハッシュにある場合はどうなりますか?どのような方法は、作るためにexpect(hash_1).to eq(hash_2)、いくつかのhash_1値はhash_2で事前にDB-時間と対応する値は、ポストDB-倍されているとき、仕事を?
マイケルジョンストン

私は任意のハッシュを考えていました(私の仕様では、操作はレコードをまったく変更しないと主張しています)。しかし、ありがとう!
マイケルジョンストン

10

古い投稿ですが、ここにソリューションを入力するすべての人に役立つことを願っています。日付を手動で作成する方が簡単で信頼性が高いと思います。

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小数を気にしたり心配したりすることなく、保存された日付が正しい日付になります。


仕様の最後に時間を固定しないでください
Qwertie '10 / 10/30

代わりにブロックを使用するように変更しました。そうすれば、通常の時間に戻ることを忘れることはできません。
jBilbo、

8

でデータベースに保存されている日付/日付時刻/時刻オブジェクトを文字列に変換できます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)

7

この問題を回避する最も簡単な方法は、次の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

1
.change(usec: 0)非常に役に立ちます
公園。

2
この.change(usec: 0)トリックを使用して、スペックヘルパーを使用しなくても問題を解決できます。最初の行がそうであれば、最後にTimecop.freeze(Time.current.change(usec: 0))単純に比較でき.to eq(Time.now)ます。
ハリーウッド

0

ハッシュを比較していたため、これらのソリューションのほとんどが機能しなかったため、比較しているハッシュからデータを取得することが最も簡単なソリューションであることがわかりました。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: ...}
)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.