ruby-on-railsdatetimeruby-on-rails-4rspectimecop

Rails - Testing a method that uses DateTime.now


I have a method that uses DateTime.now to perform a search on some data, I want to test the method with various dates but I don't know how to stub DateTime.now nor can I get it working with Timecop ( if it even works like that ).

With time cop I tried

it 'has the correct amount if falls in the previous month' do
      t = "25 May".to_datetime
      Timecop.travel(t)
      puts DateTime.now

      expect(@employee.monthly_sales).to eq 150
end 

when I run the spec I can see that puts DateTime.now gives 2015-05-25T01:00:00+01:00 but having the same puts DateTime.now within the method I'm testing outputs 2015-07-24T08:57:53+01:00 (todays date). How can I accomplish this?

------------------update---------------------------------------------------

I was setting up the records (@employee, etc.) in a before(:all) block which seems to have caused the problem. It only works when the setup is done after the Timecop do block. Why is this the case?


Solution

  • TL;DR: The problem was that DateTime.now was called in Employee before Timecop.freeze was called in the specs.

    Timecop mocks the constructor of Time, Date and DateTime. Any instance created between freeze and return (or inside a freeze block) will be mocked.
    Any instance created before freeze or after return won't be affected because Timecop doesn't mess with existing objects.

    From the README (my emphasis):

    A gem providing "time travel" and "time freezing" capabilities, making it dead simple to test time-dependent code. It provides a unified method to mock Time.now, Date.today, and DateTime.now in a single call.

    So it is essential to call Timecop.freeze before you create the Time object you want to mock. If you freeze in an RSpec before block, this will be run before subject is evaluated. However, if you have a before block where you set up your subject (@employee in your case), and have another before block in a nested describe, then your subject is already set up, having called DateTime.new before you froze time.


    What happens if you add the following to your Employee

    class Employee
      def now
        DateTime.now
      end
    end
    

    Then you run the following spec:

    describe '#now' do
      let(:employee) { @employee }
      it 'has the correct amount if falls in the previous month', focus: true do
        t = "25 May".to_datetime
        Timecop.freeze(t) do
          expect(DateTime.now).to eq t
          expect(employee.now).to eq t
    
          expect(employee.now.class).to be DateTime
          expect(employee.now.class.object_id).to be DateTime.object_id
        end
      end
    end
    

    Instead of using a freeze block, you can also freeze and return in rspec before and after hooks:

    describe Employee do
      let(:frozen_time) { "25 May".to_datetime }
      before { Timecop.freeze(frozen_time) }
      after { Timecop.return }
      subject { FactoryGirl.create :employee }
    
      it 'has the correct amount if falls in the previous month' do
        # spec here
      end
    
    end
    

    Off-topic, but maybe have a look at http://betterspecs.org/