Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 962
raphael_turtle Avatar asked Jul 24 '15 07:07

raphael_turtle


1 Answers

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/

like image 73
amiuhle Avatar answered Oct 21 '22 08:10

amiuhle