I recently needed to schedule an export to occur on Friday evenings, so I turned to the handy whenever gem and scheduled that rake task like it was nothing. The only problem was that I was using a different schedule for testing locally than for production, and the production one wasn't going to work. Luckily my coworker @exchgr pointed out during code review that you can't actually schedule tasks to run on :fridy and I addressed the issue before it ever made it to production. After blaming the typo on the lack of tools for testing the schedule that whenever generates, I decided to do something about it instead of complaining.
I spent some time trying to figure out how others were testing their whenever schedules but I was unable to find any resources. After looking at the internals of the gem, and doing some research on custom rspec matchers, I set some time aside to try to build a way to test the schedule. I came up with what I consider to be fairly fluid matchers for testing your schedule. I doubt it's the most maintainable project given the strong coupling to the gem (including some lovely uses of instance_variable_get), but it's useful nonetheless. Here is an example of the matchers in use:
# config/schedule.rb
every 1.day, at: '12:00 pm' do
rake 'challenges:send_submission_digests'
end
# spec/whenever_spec.rb
describe Whenever do
let(:whenever) do
Whenever::JobList.new(file: Rails.root.join("config", "schedule.rb").to_s)
end
it "schedules the sending of challenge submission digests" do
expect(whenever).to schedule_rake("challenges:send_submission_digests")
.every(1.day)
.at("12:00 pm")
end
end
And this is how you can go about using these matchers to test your own schedule:
# spec/support/rspec_matchers.rb
RSpec::Matchers.define :schedule_rake do |task|
match do |whenever|
jobs = whenever.instance_variable_get("@jobs")
key = @duration.is_a?(ActiveSupport::Duration) ? @duration.to_i : @duration
if jobs.has_key?(key)
jobs[key].any? do |job|
options = job.instance_variable_get("@options")
options[:task] == task && (@time.present? ? job.at == @time : true)
end
else
false
end
end
chain :every do |duration|
@duration = duration
end
chain :at do |time|
@time = time
end
failure_message do |actual|
"Expected whenever to schedule #{ task } every #{ @duration } seconds"
end
failure_message_when_negated do |actual|
"Expected whenever not to schedule #{ task } every #{ @duration } seconds"
end
end
RSpec::Matchers.alias_matcher :schedule_runner, :schedule_rake
RSpec::Matchers.alias_matcher :schedule_command, :schedule_rake
The matcher works by using the same class that the gem uses to parse your schedule and output the crontab. The only difference here is that we're using the parsed jobs list to determine what tasks are scheduled to run and when. This allows us to test that our cron jobs are scheduled at the right time of day and creates failing tests if someone changes the schedule in local development so it does not accidentally get pushed back up to master.
It's not perfect, the error messages need some updating, the code could be cleaned up, etc. Either way, it was a starting point that met all of our needs at the current moment. My goal is to tear this out into a gem so that others can use the matchers without having to copy and paste them. The code would also ideally be refactored into a PORO so it can also be tested and maintained/extended more easily.





Log in or sign up for Devpost to join the conversation.