Trouble comparing time with RSpec

Ruby on-RailsRubyTimeRspecComparison

Ruby on-Rails Problem Overview


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 ==)

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

I find using the be_within default rspec matcher more elegant:

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

Solution 2 - Ruby on-Rails

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

Solution 3 - Ruby on-Rails

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.

Solution 4 - Ruby on-Rails

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.

Solution 5 - Ruby on-Rails

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

Solution 6 - Ruby on-Rails

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)

Solution 7 - Ruby on-Rails

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

Solution 8 - Ruby on-Rails

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBackoView Question on Stackoverflow
Solution 1 - Ruby on-RailsOinView Answer on Stackoverflow
Solution 2 - Ruby on-RailsushaView Answer on Stackoverflow
Solution 3 - Ruby on-RailsjBilboView Answer on Stackoverflow
Solution 4 - Ruby on-Railsequivalent8View Answer on Stackoverflow
Solution 5 - Ruby on-RailsSamView Answer on Stackoverflow
Solution 6 - Ruby on-RailsThomas KlemmView Answer on Stackoverflow
Solution 7 - Ruby on-RailsQwertieView Answer on Stackoverflow
Solution 8 - Ruby on-RailsChris EdwardsView Answer on Stackoverflow