132
votes

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 ==)
7
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.Neil Slater
If I understood you relating "crossing second boundaries", the problem should not arise since I am using the Timecop gem that "freezes" the time.Backo
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)Neil Slater

7 Answers

166
votes

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

227
votes

I find using the be_within default rspec matcher more elegant:

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

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.

10
votes

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.
8
votes

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)
8
votes

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
0
votes

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