163

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 ==) 
3
  • It is comparing the object ids, hence the text from inspect is matching, but underneath you have two different Time objects. You could just use ===, 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. Commented Dec 5, 2013 at 14:53
  • If I understood you relating "crossing second boundaries", the problem should not arise since I am using the Timecop gem that "freezes" the time. Commented Dec 5, 2013 at 14:55
  • Ah I missed that, sorry. In which case, just use === 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) Commented Dec 5, 2013 at 15:20

8 Answers 8

278

I find using the be_within default rspec matcher more elegant:

expect(@article.updated_at.utc).to be_within(1.second).of Time.now 
Sign up to request clarification or add additional context in comments.

6 Comments

.000001 s is a bit tight. I tried even .001 and it was failing sometimes. Even .1 seconds I think proves that the time is being set to now. Good enough for my purposes.
I think this answer is better because it expresses the intent of the test, rather than obscuring it behind some unrelated methods like to_s. Also, to_i and to_s might fail infrequently if the time is near the end of a second.
Looks like the 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/…
I get undefined method 'second' for 1:Fixnum. Is there something I need to require?
@DavidMoles .second is a rails extension: api.rubyonrails.org/classes/Numeric.html
|
190

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

6 Comments

As you can see in the code from the question, I use the Timecop gem. Should it just solve the issue by "freezing" the time?
You save the time in the database and retrieve it(@article.updated_at) which makes it loose the nanoseconds where as Time.now has retains the nanosecond. It should be clear from the first few lines of my answer
The accepted answer is nearly a year older than the answer below - the be_within matcher is a much better way of handling this: A) you don't need a gem; B) it works for any type of value (integer, float, date, time, etc.); C) it's natively part of RSpec
This is an "OK" solution, but definitely the be_within is the right one
Hello I am using datetime comparisons with job enqueue delay expectations expect {FooJob.perform_now}.to have_enqueued_job(FooJob).at(some_time) I'm not sure the .atmatcher would accept a time converted to integer with .to_ianyone has faced this problem ?
|
18

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.

2 Comments

Make sure you unfreeze time at the end of the spec
Changed it to use a block instead. That way you can't forget about returning back to normal time.
11

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 

2 Comments

.change(usec: 0) is very helpful
We can use this .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.
11

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. 

2 Comments

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?
I was thinking an arbitrary hash (my spec is asserting that an operation doesn't change a record at all). But thanks!
8

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) 

Comments

0

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: ...} ) 

1 Comment

If 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: ... }))
0

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 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.