Trouble comparing time with RSpec

后端 未结 7 711
北荒
北荒 2020-12-12 15:23

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 c

相关标签:
7条回答
  • 2020-12-12 15:45

    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.

    0 讨论(0)
  • 2020-12-12 15:45

    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 讨论(0)
  • 2020-12-12 15:56

    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)
    
    0 讨论(0)
  • 2020-12-12 15:57

    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

    0 讨论(0)
  • 2020-12-12 15:57

    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: ...}
    )
    
    0 讨论(0)
  • 2020-12-12 16:02

    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.
    
    0 讨论(0)
提交回复
热议问题