I am using Ruby on Rails 4 and the rspec-rails gem 2.14. For a my object I would like to compare the current time with the updated_at
object attribute after a controller action run, but I am in trouble since the spec does not pass. That is, given the following is the spec code:
it "updates updated_at attribute" do
Timecop.freeze
patch :update
@article.reload
expect(@article.updated_at).to eq(Time.now)
end
When I run the above spec I get the following error:
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 ==)
How can I make the spec to pass?
Note: I tried also the following (note the utc
addition):
it "updates updated_at attribute" do
Timecop.freeze
patch :update
@article.reload
expect(@article.updated_at.utc).to eq(Time.now)
end
but the spec still does not pass (note the "got" value difference):
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 ==)
===
, but that may suffer from crossing second boundaries. Probably best is to find or write your own matcher, in which you convert to epoch seconds and allow for a small absolute difference.
===
instead of ==
- currently you are comparing the object_id of two different Time objects. Although Timecop won't freeze database server time . . . so if your timestamps are being generated by the RDBMS it wouldn't work (I expect that is not a problem for you here though)
I find using the be_within
default rspec matcher more elegant:
expect(@article.updated_at.utc).to be_within(1.second).of Time.now
Ruby Time object maintains greater precision than the database does. When the value is read back from the database, it’s only preserved to microsecond precision, while the in-memory representation is precise to nanoseconds.
If you don't care about millisecond difference, you could do a to_s/to_i on both sides of your expectation
expect(@article.updated_at.utc.to_s).to eq(Time.now.to_s)
or
expect(@article.updated_at.utc.to_i).to eq(Time.now.to_i)
Refer to this for more information about why the times are different
Timecop
gem. Should it just solve the issue by "freezing" the time?
be_within
is the right one
expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time)
I'm not sure the .at
matcher would accept a time converted to integer with .to_i
anyone has faced this problem ?
Old post, but I hope it helps anyone who enters here for a solution. I think it's easier and more reliable to just create the date manually:
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
This ensures the stored date is the right one, without doing to_x
or worrying about decimals.
yep as Oin
is suggesting be_within
matcher is the best practice
...and it has some more uscases -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher
But one more way how to deal with this is to use Rails built in midday
and middnight
attributes.
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
Now this is just for demonstration !
I wouldn't use this in a controller as you are stubbing all Time.new calls => all time attributes will have same time => may not prove concept you are trying to achive. I usually use it in composed Ruby Objects similar to this:
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
But honestly I recommend to stick with be_within
matcher even when comparing Time.now.midday !
So yes pls stick with be_within
matcher ;)
update 2017-02
Question in comment:
what if the times are in a Hash? any way to make expect(hash_1).to eq(hash_2) work when some hash_1 values are pre-db-times and the corresponding values in hash_2 are post-db-times? –
expect({mytime: Time.now}).to match({mytime: be_within(3.seconds).of(Time.now)}) `
you can pass any RSpec matcher to the match
matcher (so e.g. you can even do API testing with pure RSpec)
As for "post-db-times" I guess you mean string that is generated after saving to DB. I would suggest decouple this case to 2 expectations (one ensuring hash structure, second checking the time) So you can do something like:
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)
But if this case is too often in your test suite I would suggest writing your own RSpec matcher (e.g. be_near_time_now_db_string
) converting db string time to Time object and then use this as a part of the 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)
work when some hash_1 values are pre-db-times and the corresponding values in hash_2 are post-db-times?
The easiest way I found around this problem is to create a current_time
test helper method like so:
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
Now the time is always rounded to the nearest millisecond to comparisons are straightforward:
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)
is very helpful
.change(usec: 0)
trick to solve the problem without using a spec helper too. If the first line is Timecop.freeze(Time.current.change(usec: 0))
then we can simply compare .to eq(Time.now)
at the end.
You can convert the date/datetime/time object to a string as it's stored in the database with 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)
Because I was comparing hashes, most of these solutions did not work for me so I found the easiest solution was to simply grab the data from the hash I was comparing. Since the updated_at times are not actually useful for me to test this works fine.
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
is not useful for your use case, you can go for a more elegant solution instead of comparing it with itself: expect(data).to match(hash_including({ some_other_keys: ... }))
In Rails 4.1+ you can use 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
Success story sharing
to_s
. Also,to_i
andto_s
might fail infrequently if the time is near the end of a second.be_within
matcher was added to RSpec 2.1.0 on November 7, 2010, and enhanced a few times since then. rspec.info/documentation/2.14/rspec-expectations/…undefined method 'second' for 1:Fixnum
. Is there something I need torequire
?.second
is a rails extension: api.rubyonrails.org/classes/Numeric.html