How to override Date.today when running rails tests? - ruby-on-rails

I have some tests that were failing on specific dates, because someone wrote them to use Date.today. I want to reproduce the failures on previous select dates.
Is there a way to run rake test with an ENV variable that will override the system clock? So that calls to Date.today, Time.now, and 1.day.ago and 1.day.from_now will use the date I set?
Like, for example:
> DATE_TODAY='2017-01-04' rake test

For testing you can use timecop gem.
It offers you two useful methods Timecop.freeze and Timecop.travel.
For example, you can use freeze to statically set time in before hooks:
describe 'your tests' do
before do
Timecop.freeze(Time.new(2017, 1, 4))
end
after do
Timecop.return
end
it 'should do something' do
sleep(10)
Time.now # => 2017-01-04 00:00:00
end
end
Or in a block:
Timecop.freeze(Time.now - 3.months) do
assert product.expired?
end
While with the travel method, you change the starting moment, but time is still passing by.
Timecop.travel(Time.new(2017, 1, 4))
sleep(10)
Time.now # => 2017-01-04 00:00:10

As of Rails 4.1 you can do
travel_to Time.new(2004, 11, 24, 01, 04, 44)
The full API docs are here: http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html

Related

Rails 5 - Sidekiq worker shows job done but nothing happens

I'm using Sidekiq for delayed jobs with sidekiq-status and sidekiq-ent gems. I've created a worker which is reponsible to update minor status to false when user is adult and has minor: true. This worker should be fired every day at midnight ET. Like below:
#initializers/sidekiq.rb
config.periodic do |mgr|
# every day between midnight 0 5 * * *
mgr.register("0 5 * * *", MinorWorker)
end
#app/workers/minor_worker.rb
class MinorWorker
include Sidekiq::Worker
def perform
User.adults.where(minor: true).remove_minor_status
rescue => e
Rails.logger.error("Unable to update minor field. Exception: #{e.message} : #{e.backtrace.join('\n')}")
end
end
#models/user.rb
class User < ApplicationRecord
scope :adults, -> { where('date_of_birth <= ?', 18.years.ago) }
def self.remove_minor_status
update(minor: false)
end
end
No I want to check this on my local machine - to do so I'm using gem 'timecop' to timetravel:
#application.rb
config.time_zone = 'Eastern Time (US & Canada)'
#config/environments/development.rb
config.after_initialize do
t = Time.local(2021, 12, 21, 23, 59, 0)
Timecop.travel(t)
end
After firing up sidekiq by bundle exec sidekiq and bundle exec rails s I'm waiting a minute and I see that worker shows up:
2021-12-21T22:59:00.130Z 25711 TID-ovvzr9828 INFO: Managing 3 periodic jobs
2021-12-21T23:00:00.009Z 25711 TID-ovw69k4ao INFO: Enqueued periodic job SettlementWorker with JID ddab15264f81e0b417e7dd83 for 2021-12-22 00:00:00 +0100
2021-12-21T23:00:00.011Z 25711 TID-ovw69k4ao INFO: Enqueued periodic job MinorWorker with JID 0bcd6b76d6ee4ff9e7850b35 for 2021-12-22 00:00:00 +0100
But it didn't do anything, the user's minor status is still set to minor: true:
2.4.5 :002 > User.last.date_of_birth
=> Mon, 22 Dec 2003
2.4.5 :001 > User.last.minor
=> true
Did I miss something?
EDIT
I have to add that when I'm trying to call this worker on rails c everything works well. I've got even a RSpec test which also passes:
RSpec.describe MinorWorker, type: :worker do
subject(:perform) { described_class.new.perform }
context 'when User has minor status' do
let(:user1) { create(:user, minor: true) }
it 'removes minor status' do
expect { perform }.to change { user1.reload.minor }.from(true).to(false)
end
context 'when user is adult' do
let(:registrant2) { create(:registrant) }
it 'not change minor status' do
expect(registrant2.reload.minor).to eq(false)
end
end
end
end
Since this is the class method update won't work
def self.remove_minor_status
update(minor: false)
end
Make use of #update_all
def self.remove_minor_status
update_all(minor: false)
end
Also, I think it's best practice to have some test cases to ensure the working of the methods.
As of now you can try this method from rails console and verify if they actually work
test "update minor status" do
user = User.create(date_of_birth: 19.years.ago, minor: true)
User.adults.where(minor: true).remove_minor_status
assert_equal user.reload.minor, false
end
I think you need to either do update_all or update each record by itself, like this:
User.adults.where(minor: true).update_all(minor: false)
or
class MinorWorker
include Sidekiq::Worker
def perform
users = User.adults.where(minor: true)
users.each { |user| user.remove_minor_status }
rescue => e
Rails.logger.error("Unable to update minor field. Exception: #{e.message} : #{e.backtrace.join('\n')}")
end
end
You may also want to consider changing update to update! so it throws an error if failing to be caught by your rescue in the job:
def self.remove_minor_status
update!(minor: false)
end

Rails/Rspec - objects not existing until interacted with

I have the following code where I create some instances of records using FactoryBot:
describe "#force_recalculation_of_lab_container_labs" do
(1..5).each do |n|
let("lab_#{n}".to_sym) { create(:lab) }
let("lab_container_#{n}".to_sym) { create(:skill_path) }
let("lab_collection_#{n}".to_sym) do
create(:lab_collection, lab_container: eval("lab_container_#{n}"))
end
end
context 'when adding labs' do
it "starts with 0 labs" do
expect(lab_collection_1.labs.count).to eq(0)
end
(1..3).each do |n|
let("lab_collection_inclusion_#{n}") do
create(:lab_collection_inclusion,
included_item_id: eval("lab_#{n}").id,
included_item_type: 'Lab',
lab_collection_id: eval("lab_collection_1").id,
subscribed: 0)
end
end
it "updates the lab total correctly after adding labs" do
binding.pry
end
end
end
From my pry point, I receive the following:
LabCollectionInclusion.count
=> 0
lab_collection_1.lab_collection_inclusions.count
=> 0
When I then call a record individually, it appears to then exist:
lab_collection_inclusion_1
<LabCollectionInclusion:0x000055a45c985b10
id: 1,
included_item_id: 1,
included_item_type: "Lab",
lab_collection_id: 4,
subscribed: false,
created_at: Thu, 01 Nov 2018 10:48:00 UTC +00:00,
updated_at: Thu, 01 Nov 2018 10:48:00 UTC +00:00>
After which point it exists when searching:
LabCollectionInclusion.count
=> 1
lab_collection_1.lab_collection_inclusions.count
=> 1
Obviously I don't want to have to do this for every record so my 2 questions are first of all why is this happening, and second of all how to correct it?
Thanks in advance
This is expected behavior because let is designed to lazy-evaluated. Quote from the docs of let:
Note that let is lazy-evaluated: it is not evaluated until the first time the method it defines is invoked. You can use let! to force the method's invocation before each example.
As described in the documentation: Just let! (note the !) instead of let when you need the records to be created without calling them by their name first.

model method trigger boolean to true?

How can I trigger method accomplished_challenge upon days_left_challenged == 0?
challege.rb
before_save :days_left_challenged_sets_deadline
# makes ongoing_challenge become past (before_save) THIS WORKS
def days_left_challenged_sets_deadline
if self.date_started != nil
if days_left_challenged <= 0
self.accomplished = true
self.deadline = self.date_started
end
end
end
# makes ongoing_challenge become past (whenever gem) THIS DOESN'T
def self.accomplished_challenge
self.all.each do |challenge|
if challenge.days_left_challenged <= 0
challenge.accomplished = true
challenge.deadline = self.date_started
end
end
end
# Counts down how many days left in days_challenged using committed
def days_left_challenged
self.days_challenged - ((date_started.to_date)..Date.yesterday).count do |date|
committed_wdays.include? date.wday
end + self.missed_days
end
Challenge.last
id: 1,
action: "Run",
committed: ["sun", "mon", "tue", "wed", "thu", "fri", "sat", ""],
date_started: Sat, 06 Feb 2016 00:00:00 EST -05:00,
deadline: nil,
accomplished: nil,
days_challenged: 10,
missed_days: 0,
I can't trigger it with a callback or validation I don't think since days_left_challenged can turn to 0 at any point in the life of a challenge.
I suggest you use a gem like Whenever to setup a cron job to run every day or so and do that checking for all Challenges. It would be something like:
every 1.day, :at => '0:01 am' do
runner "Challenge.accomplished_challenge"
end
And your accomplished_challenge must be a class method that checks all (or the one you choose using a filter) Challenges:
def self.accomplished_challenge
self.all.each do |challenge|
if challenge.days_left_challenged == 0
challenge.update_attributes(deadline: self.date_started, accomplished: true)
end
end
end
---- EDIT to work on Heroku ----
Create a task on /lib/tasks/scheduler.rake:
# /lib/tasks/scheduler.rake
desc "This task is called by the Heroku scheduler add-on"
task :check_accomplished_challenges => :environment do
puts "Checking accomplished challenges..."
Challenge.accomplished_challenge
puts "done."
end
Go to your heroku app Resources page and add 'Heroku Scheduler'. Open the scheduler and add the task:
rake check_accomplished_challenges
Set it to run every day.
More details: https://devcenter.heroku.com/articles/scheduler

Invalid Time.now on RSpec

How do I set the correct Time on RSpec.
[2] pry(#<RSpec::ExampleGroups::Device::Creation::Security>)> Time.now
=> Fri, 01 Jan 2016 10:00:00 CET +01:00
On rails console I have the correct time:
[1] pry(main)> Time.now
=> 2015-12-11 12:41:36 -0500
I don't want to stub it, because I'm not passing the time to my class. Have the following method in the class I'm testing:
def within_expiration?
Time.now - user.sms_sent_date < time_limit_in_seconds
end
This is not behaving as expected because Time.now is Jan 1, 2016
user.sms_sent_date has right date.
Make sure you don't have any library that is currently altering your Time in the spec.
For example, one of the most commonly used is Timecop. Make sure you don't setup it globally, instead use the blocks or before/after to properly mock the date and teardown the mock when no longer required.

Override what Ruby thinks is the current time in Time.now?

I want to create test data for an application, and there are a lot of time_at attributes being tracked, too many to override in a maintainable way. What I'm thinking is, can I just change the base reference time variable in Ruby?
This would make it so created_at, updated_at, last_login_at, etc., could be set to an artificial time, so I could do this in tests:
Date.today #=> Thu, 30 Dec 2010
Time.system_time_offset = 1.week.ago # made up
Date.today #=> Thu, 23 Dec 2010
Time.now #=> Thu Dec 23 14:08:38 -0600 2010
user_1 = User.create!
user_1.created_at #=> Thu Dec 23 14:08:38 -0600 2010
Time.reset_system_time # made up
user_2 = User.create!
user_1.created_at #=> Thu Dec 30 14:08:38 -0600 2010
Is there a way to do this?
You could use Mocha to change the return value of Time.now during a test:
Time.stubs(:now).returns(Time.now - 1.day)
A good gem for this is Timecop: https://github.com/travisjeffery/timecop.
You can freeze time or change the time (while it continues to progress) very easily.
Ex.
Time.now
# => 2014-03-14 13:17:02 -0400
Timecop.travel 2.hours.ago
Time.now
# => 2014-03-14 11:17:04 -0400
Its nicer than the mocha solution since all time functions will be affected equally, so you won't have a test where Time.now is returning something different then DateTime.now
Its also more up-to-date than the time-warp gem suggested in another answer.
I use the timewarp gem for this sort of thing. You just put your code in a pretend_now_is(time) block and the code inside will be executed as if that was the actual time.
http://github.com/harvesthq/time-warp
Here's an example
def test_should_find_company_needing_reminded_today
pretend_now_is(Time.utc(2008,"jul",24,20)) do #=> Thu Jul 24 20:00:00 UTC 2008
#company.reminder_day = 'Thursday'
#company.save
companies = Company.find_companies_needing_reminded_today
assert_equal true, companies.include?(#company)
end
end
Honestly, I usually write tests for current time to check if the timestamp is within a reasonable range. i.e., check if the timestamp is greater than 1.minute.ago. Changing the system clock is likely to have all kinds of unpredictable side-effects, so you don't want to do that. You might be able to track down all the places in Ruby where the current time is accessed (though I think most methods just use Time.now) and monkey-patch them for the tests, but I'd probably still prefer just checking the timestamp is within a sane range.
It's also possible to (yuck) monkeypatch Time:
$start = Time.now - 86400 # this time yesterday
class Time
class << Time
def new
$start
end
def now
Time.new
end
end
end
puts(Time.now)
puts($start)

Resources