Rails - Testing a method that uses DateTime.now - ruby-on-rails

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?

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/

Timecop should be able to handle what you want. Try to freeze the time before running your test instead of just traveling, then unfreeze when you finish. Like this:
before do
t = "25 May".to_datetime
Timecop.freeze(t)
end
after do
Timecop.return
end
it 'has the correct amount if falls in the previous month' do
puts DateTime.now
expect(#employee.monthly_sales).to eq 150
end
From Timecop's readme:
freeze is used to statically mock the concept of now. As your program executes, Time.now will not change unless you make subsequent calls into the Timecop API. travel, on the other hand, computes an offset between what we currently think Time.now is (recall that we support nested traveling) and the time passed in. It uses this offset to simulate the passage of time.
So you want to freeze the time at a certain place, rather than just travel to that time. Since time will pass with a travel as it normally would, but from a different starting point.
If this still does not work, you can put your method call in a block with Timecop to ensure that it is freezing the time inside the block like:
t = "25 May".to_datetime
Timecop.travel(t) do # Or use freeze here, depending on what you need
puts DateTime.now
expect(#employee.monthly_sales).to eq 150
end

I ran into several problems with Timecop and other magic stuff that messes with Date, Time and DateTime classes and their methods. I found that it is better to just use dependency injection instead:
Employee code
class Employee
def monthly_sales(for_date = nil)
for_date ||= DateTime.now
# now calculate sales for 'for_date', instead of current month
end
end
Spec
it 'has the correct amount if falls in the previous month' do
t = "25 May".to_datetime
expect(#employee.monthly_sales(t)).to eq 150
end
We, people of the Ruby world, find great pleasure in using some magic tricks, which people who are using less expressive programming languages are unable to utilize. But this is the case where magic is too dark and should really be avoided. Just use generally accepted best practice approach of dependency injection instead.

Related

How to stub out Date that is implicitly referenced by years.ago in RSpec?

I know that I can stub out a method on Date like so:
allow(Date).to receive(:today).and_return Date.new(2015,11,10)
So now if within my spec the code calls Date.today I can be assured that it will return a Date object with the value of 11/10/2015.
I have a scope that utilizes years.ago.to_date. The user specifies the number of years ago for the scope: Ex: 5.years.ago, 2.years.ago.
I am trying to test this scope. In order to do so I need to control the Date that years.ago is referencing. For instance, I would always want the Date to be 1/1/2010. This way I will know that 5.years.ago will return 1/1/2005, and 2.years.ago would return 1/1/2008.
The issue is that I do not know what to stub out. I do not know how to keep the Date consistent which years.ago uses.
Hopefully this makes sense. I just need to control the Date that years.ago uses. How can I stub that out?
I looked a bit at ActiveSupport::Duration, but I'm not sure if that is the right place to look.
You should check timecop
Then in your tests, you could freeze the date to your desired value as follows:
describe "some set of tests to mock" do
before do
Timecop.freeze(2010, 1, 1)
end
after do
Timecop.return
end
it "should do blah blah blah" do
end
end
You can usually control what Ruby and Rails use for the current time by stubbing Time.now:
allow(Time).to receive(:now).and_return(Time.local 2016, 9, 6, 16, 51)
That does work for years.ago.
If your code, or the framework code you use, uses both Time.now and Date.today, however, timecop is easier.
If you use timecop, be aware that it's easy to forget to Timecop.return, which can screw up subsequent tests. Prefer timecop's safe mode.

How do I test complex relationships in Rails with RSpec?

Firstly, this question may stray into opinion but I think it's a valuable question to ask. I will give a very specific example for my application which handles absence management and tracking.
An Account has many Users and a User has many Absences. The Account can create PublicHolidays which should be ignored when calculating the number of days that an Absence uses.
Example: If a person takes a week off, the days used will be 5. If one of those days is a PublicHoliday, the days used would be 4.
I want to implement a method such that when a PublicHoliday is created, the days used for any Absences created prior to the date of creation and which cross the date of the PublicHoliday are recalculated.
My current RSpec test looks like this:
it 'triggers a recalculation of absence days on create for absences created before the date of creation of the public holiday' do
robin = FactoryGirl.create(:robin)
absence = FactoryGirl.create(:basic_absence, user: robin)
expect(absence.days_used).to eq(1)
ph = FactoryGirl.create(:public_holiday, country: "England", account: robin.account)
expect(absence.reload.days_used).to eq(0)
end
In this test, ph is the same date as the absence so I expect it to calculate one day to start with and then I intend to use an after create callback to recalculate the days used.
Is this the right way to do this test? Is there a more efficient way without creating a number of associated objects?
Firstly - it's good practice to use lets instead of local variables, and secondly - split your tests so each test tests just one thing. Thirdly: anything that sets up a context for tests should be put into a context-block (even if there's only one test in that context)
eg, here's a re-writing of your spec the standard way:
let(:robin) { FactoryGirl.create(:robin) }
let(:absence) { FactoryGirl.create(:basic_absence, user: robin) }
context "with no public holidays" do
it 'counts the absence day to be a day used' do
expect(absence.days_used).to eq(1)
end
end
context "with a public holiday for the absence" do
before do
FactoryGirl.create(:public_holiday, country: "England", account: robin.account)
end
it 'does not consider the absence day to be a day used' do
expect(absence.days_used).to eq(0)
end
end

TimeWithZone & Time.zone.now integration test fails

In a controller method I set a user's variable activation_sent_at equal to Time.zone.now when an activation email is sent to that user. On the development server this seems to work (although time expressions in my application are 2 hours behind on the local time of my computer).
I want to include an integration test that tests whether activation_sent_at indeed gets set properly. So I included the line:
assert_equal #user.activation_sent_at, Time.zone.now
However, this produces the error:
No visible difference in the ActiveSupport::TimeWithZone#inspect output.
You should look at the implementation of #== on ActiveSupport::TimeWithZone or its members.
I think it's suggesting to use another expression for Time.zone.now in my test. I've looked at different sources, including http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html, but am not sure what to do here. Any suggestions what could be causing this error?
Additional info: Adding puts Time.zone.now and puts #stakeholder.activation_sent_at confirms the two are equal. Not sure what generates the failure/error.
The issue is that the 2 dates are very close to each other but not the same. You can use assert_in_delta
assert_in_delta #user.activation_sent_at, Time.zone.now, 1.second
For RSpec, a similar approach would be to use be_within:
expect(#user.activation_sent_at).to be_within(1.second).of Time.zone.now
The problem is that your times are very close but not quite equal. They are probably off by a few fractions of a second.
One solution to issues like this is a testing gem called timecop. It gives you the ability to mock Time.now so that it will temporarily return a specific value that you can use for comparisons.
The reason is because Time.now or Time.zone.now include milliseconds (when you do a simple put to print the time it doesn't show milliseconds). However, when you persist the timestamp in the database these milliseconds likely get lost unless the db field is configured to store milliseconds. So when you read the value from the db it will not include milliseconds, hence the times are slightly different.
One solution is to remove milliseconds from Time.now. You can do this like so Time.now.change(usec: 0). This should fix the error in the tests.

Testing Internationalised Dates in Rspec

I have some business logic in a controller that I want to test that involves setting two values to today's date and yesterday's date.
Initially I had a passing test that essentially looked like this;
controller:
def wibble
#start_time = Date.yesterday
end
test:
it 'blah blah'
get :wibble
assigns(:start_date).should eq(Date.yesterday)
end
But a new requirement has been added that means the date should be i18n'd, which means that the controller is returning back something different.
My Thoughts
I had thought about mocking the variables, because I could only care that they are set to something, but then the business logic of Today and Yesterday isn't being exercised.
I also considered forcing i18n on the test, but this seems way to brittle.
Can anyone suggest a good way to test this?

How to test uniqueness of Coupon/Promo-Codes?

I have a Model PromoCode which has a .generate! method, that calls .generate which generates a String using SecureRandom.hex(5) and saves it to the database:
class PromoCode < ActiveRecord::Base
class << self
def generate
SecureRandom.hex 5
end
def generate!
return create! code: generate
end
end
end
Now I want to write a spec that test the uniqueness of the generated string. The .generate method should be called as long as a non existent PromoCode has been generated.
I'm not sure how to do this since I can't really stub out the .generate method to return fixed values (because then it would be stuck in an infinite loop).
This is the passing spec for the model so far:
describe PromoCode do
describe ".generate" do
it "should return a string with a length of 10" do
code = PromoCode.generate
code.should be_a String
code.length.should eql 10
end
end
describe ".generate!" do
it "generates and returns a promocode" do
expect {
#promo = PromoCode.generate!
}.to change { PromoCode.count }.from(0).to(1)
#promo.code.should_not be_nil
#promo.code.length.should eql 10
end
it "generates a uniq promocode" do
end
end
end
Any directions appreciated.
Rspec's and_return method allows you to specify multiple return values that will be cycled through
For example you could write
PromoCode.stub(:generate).and_return('badcode1', 'badcode2', 'goodcode')
Which will cause the first call to generate to return 'badcode1', the second 'badcode2' etc... You can then check that the returned promocode was created with the correct code.
If you want to be race condition proof you'll want a database uniqueness constraint, so your code might actually want to be
def generate!
create!(...)
rescue ActiveRecord::RecordNotUnique
retry
end
In your spec you would stub the create! method to raise the first time but return a value the second time
And what about something like: create a PromoCode, save the result, and try to create a new PromoCode with the code of the previous PromoCode object:
it "should reject duplicate promocode" do
#promo = PromoCode.generate!
duplicate_promo = PromoCode.new(:code => #promo.code)
duplicate_promo.should_not be_valid
end
Also, this is model level, I am assuming you have a key in the database that will save you from race conditions...
If you saves the promocode in database you would have added validations there in the model for uniq promocode. So you can test the same in rspec too.
Like this,
it { should validate_uniqueness_of(:promocode) }
This answer is based on your comment:
I need to make sure that generate! generates a code - no matter what,
until a unique code has been generated.
I feel like you might have a hard time testing this correctly. Unit testing "no matter what" situations with indefinite loops can be a bit of a tricky subject.
I'm not sure how to do this since I can't really stub out the .generate method to return fixed values (because then it would be stuck in an infinite loop).
One possibility to consider might be if instead of doing either one or the other, you tried both? (That is, find a way make it return a fixed number under certain circumstances, and eventually trigger the actual random number. An instance variable counter might help; set it to a random number, count it down, when it's greater than zero return the fixed number, or something along those lines). This still doesn't feel like a perfect test, though, or even a very good one for that matter.
It might be worth looking more into means of generating similar strings with high probability of them being unique, and having some mathematical proof of it being that way. I'm not saying this is the most practical idea either, but if you really need to prove (as you're trying to do with tests), it might be the more probably solution.

Resources