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.

Share this project:
×

Updates

Matthew Gerrior posted an update

I've published this gem to RubyGems since someone else actually expressed interest in it. Now you can just add gem "shoulda-whenever", "~> 0.0.1" to your Gemfile and be good to go without having to use the source on Github.

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

Matthew Gerrior posted an update

I made this into a Gem today for anyone else that feels like they should be testing their Whenever schedule. Which, judging by the lack of resources available when I was googling the issue, isn't a lot of people.

Anyway, its not on RubyGems yet, so you'll have to include it in your Gemfile like so:

# Gemfile

gem "shoulda-whenever", git: "git@github.com:MGerrior/shoulda-whenever.git", branch: "master"

After that, you can start testing schedules like this one:

# config/schedule.rb

every :friday, at: "12:00 PM", roles: [:app, :database] do
  rake "do:something:useful"
end

With specs like this:

# spec/schedule_spec.rb

describe "Schedule" do
  include Shoulda::Whenever

  let(:whenever) { Whenever::JobList.new(file: Rails.root.join("config", "schedule.rb").to_s) }

  it "does something useful every friday" do
    expect(whenever).to schedule_rake("do:something:useful")
      .every(:friday)
      .at("12:00 PM")
      .with_roles([:app, :database])
  end
end

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