Ruby 4とrspec-Rails gem 2.14でRailsを使用しています。私のオブジェクトの場合、コントローラーアクションの実行後に現在の時刻を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 ==)
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 を参照してください
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)
patch :update
@article.reload
expect(@article.updated_at).to eq(freezed_time)
end
これにより、to_x
を実行したり、小数を心配したりすることなく、保存された日付が正しい日付になります。
うん、Oin
はbe_within
マッチャーがベストプラクティスであることを示唆している
...さらにいくつかのケースがあります-> http://www.eq8.eu/blogs/27-rspec-be_within-matcher
しかし、これに対処するもう1つの方法は、midday
およびmiddnight
属性に組み込まれた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呼び出しをスタブ化しているため、コントローラーではこれを使用しません=>すべての時間属性が同じ時間を持ちます=>達成しようとしている概念が証明されない場合があります。私は通常、これに似た構成された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を使用することをお勧めします。
はい、plsはbe_within
matcherに固執します;)
2017-02を更新
コメントの質問:
時間がハッシュ内にある場合はどうなりますか? hash_1値の一部が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)
ただし、このケースがテストスイートで頻繁に発生する場合は、db文字列時間をTimeオブジェクトに変換する own RSpec matcher (eg be_near_time_now_db_string
)を記述し、これを一部として使用することをお勧めします。 match(hash)
:
expect(hash).to match({mytime: be_near_time_now_db_string}) # you need to write your own matcher for this to work.
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)
この問題を回避する最も簡単な方法は、次のような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