Temporarily setting system time with Ruby for unit testing - ruby-on-rails

I've created a custom validation on a Apt model that verifies that an appointment can't be booked for the next day if the form submission is done after 3 PM and I'm trying to determine the best way to run the unit tests to verify that my custom validation is working correctly.
Currently, my test contains an if statement to to check if the test is run before or after 3 PM and uses a different test based on the current system time. However, I'd like to be able to test both cases no matter what the system time is.
test "booking appointment for tomorrow before/after three pm" do
a = appointments(:aptone)
# Set appointment date to tomorrow
a.apt_date = DateTime.current + 1.day
if (Time.current.hour >= 12)
assert(a.invalid?, 'Appointment CANNOT be booked after 3 PM')
elsif
assert(a.valid?, 'Appointment CAN be booked before 3 PM')
end
end
I'm wondering if there is a way to temporarily set the system time while running the test so that I can test both assertions regardless of when the tests are ran.

I can strongly recommend the timecop gem which was written for exactly the requirements you mentioned. Have a look at the documentation on the github page, specifically the freeze method (or travel, but it is better to test against a frozen system time in your example).

An easier approach would be to use a mocking framework to stub the response from the DateTime.current call to return your hardcoded value.
That avoids messing with your system clock.

System Time is related to your OS and not to your application. So if you wanted to set it then you can set it using
date --set="STRING"
in Linux.

Related

Execute a function when a time is equal to a certain value in Rails 6

I am building an online e-commerce store, and I am trying to use rails with action cable to update a product from being out of stock to in-stock at a certain date time e.g 12:00:00 2020-02-19.
The idea is as soon as the time is reached, I want to push a Websocket that the product is now available.
I have tried a few solutions such as:
Thread.new do
while true do
if **SOMETIME** == Time.now
ActionCable.server.broadcast "product_channel",content: "product-in-stock"
end
end
end
The main issue with this approach is that it creates another thread and makes rails unresponsive. Furthermore, if this value is set for say 1 week from now I do not want every user who queries the endpoint to create a brand-new thread running like this.
You have two option use sidekiq jobs or use whenever job gem
https://github.com/mperham/sidekiq/wiki/Scheduled-Jobs
Whenever allow you to set specific day and time, check the documentation for more info
https://github.com/javan/whenever

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.

Multi timezone task scheduling in Rails (using whenever)

We're currently using 'whenever' to schedule jobs in a Rails project. The system is expanding to support users in multiple timezones (timezone is stored in User model) and we would like to send users an email at a specific time of THEIR day.
Any ideas on the best pattern to achieve this? I'm foreseeing 24 (or more - there are half timezones) 'whenever' tasks per email job each with a different timezone filter on the user table.
Any ideas for a cleaner solution? A better scheduler than whenever perhaps? Something that will create the 24/48 cronjobs and call a callback passing a timezone or a UTC offset? Something like that.
I am not familiar with 'whenever' but I had a similar challenge and used clockwork
My problem was that I had to run a process at midnight for each of my customers. And like you, my customers can be anywhere in the world so it had to be ran at midnight their time.
The process of running the job at midnight looked something like this:
TZInfo::Timezone.all_country_zone_identifiers.each do |zone|
every(1.day, {time_zone: zone}, at: '00:00', tz: "#{zone}") do |job_attr|
time_zone = job_attr[:time_zone]
YourJob.perform_later(time_zone)
end
end
The class of 'YourJob' will just find all my customers with that time_zone and perform a job.
Hope that helps.

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.

How do I work with Time in Rails?

I've been pulling my hair out trying to work with Time in Rails. Basically I need to set all time output (core as well as ActiveSupport) to the server's local time -- no GMT, no UTC, etc. I've seen various posts relating to Time, but they usually involve someone's need to set it for each user. Mine isn't nearly as complex, I simply want consistency when I use any Time object. (I'd also appreciate not receiving errors every 3 seconds telling me that I can't convert a Fixnum (or some other type) to string -- it's Ruby, just do it!)
I also seem to be getting drastically different times for Time.new vs the ActiveSupport 1.second.ago. Anyway, does anyone have any quality suggestions as regards working with Time in Rails?
If you just want Time objects to be consistent, then why not stick with UTC? I just tried Time.new and 1.second.ago using script/console and I get the same output (give or take a second for typing the command). How are you doing it?
Somewhere in your initializers, define the format(s) that you want to use.
ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!(:default => '%m/%d/%Y %H:%M')
ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!(:my_special_format => '%H:%M %p')
Then when you want to print a Time object, it works like the following example. Notice that the Time object in my console is already aware of my time zone. I'm not performing any magical transformations here.
>> t = Time.now
=> Wed Jul 15 18:47:33 -0500 2009
>> t.to_s
=> "07/15/2009 18:47"
>> t.to_s(:my_special_format)
=> "18:47 PM"
Calling Time#to_s uses the :default format, or you can pass in the name of the format you'd rather use like I did with :my_special_format.
You can see the various options for formatting a Time object here.
If u don't want to store each user time setting, the only solution is to use javascript time system because it work on user client time. For example i have an application that each time user try it, the app will create some example data with each data have a initial date value "today". At first time, it confuse me a lot because my host server is in australia and lot of user is on western part, so sometime the initial date value is not "today", it said "yesterday" because of different time region.
After a couple day of headache i finally take decision to JUST use javascript time system and include it in the link, so when user click the "try now" link it will also include today date value.
<% javascript_tag do -%>
var today = new Date();
$("trynow").href = "<%= new_invitation_path %>?today=" + today.toLocaleString();
<% end -%>
Add the following to config/environment.rb to handle time correctly and consistently all the time within the context of Rails. It's important to know that it will store your times to the database in UTC -- but this is what you want -- all the conversion is done automatically.
config.time_zone = 'Pacific Time (US & Canada)'
You can run rake time:zones:local from your Rails root directory to get a list of valid time zone strings in your area.
A quick addition to the DATE_FORMAT solution posted above. Your format can be a string, in which case it works as noted above by calling strftime, but you can also define the format as a lambda:
CoreExtensions::Time::Conversions::DATE_FORMATS.merge! :my_complex_format => lambda {|time|
# your code goes here
}

Resources