---
title: Testing Cron on Heroku
teaser:
tags: web,testing,ruby,heroku
author: Nick Quaranto
published_on: 2011-07-05
---

Deploying cron to Heroku is really…pleasant. Click “Daily” or “Hourly” cron and
without any tedious setup of scripts, ensuring output is logged, or referring to
[CronWTF](http://cronwtf.github.com/). Testing it though, is a pain! No longer
should this be the case.

![''](https://img.skitch.com/20110705-k6kes3e2seeqqp1kixcxm51agx.png)

<small>[Cron is serious business.](http://www.nataliedee.com/archives/2007/jul/)</small>

Outside of Heroku, splitting up daily and hourly cron tasks is easy: just have a
`script/cron_hourly` and `script/cron_daily` in your Rails app, and have fun
configuring that on your server. On Heroku, it's handled in **one** Rake task.
Here's an example from the [Dev
Center:](http://devcenter.heroku.com/articles/cron)

    desc "This task is called by the Heroku cron add-on"
    task :cron => :environment do
      if Time.now.hour % 4 == 0 # run every four hours
        puts "Updating feed..."
        NewsFeed.update
        puts "done."
      end

    if Time.now.hour == 0 # run at midnight
        User.send_reminders
      end
    end

Two things stand out here. First, checking for hourly/daily tasks is done by
looking at `Time.now`. Second, that's a lot of logic to put in a Rake task, and
not write a test!

We've talked about testing [Rake
integration](https://thoughtbot.com/blog/post/159805695/testing-rakes-integration)
before, and we're going to use a similar pattern here: extract the Rake task
into a model. For [Radish](http://radishapp.com), our `Rakefile` now has:

    desc "Run cron job"
    task :cron => :environment do
      Cron.run
    end

Our `Cron` class now has to handle the daily and hourly tasks. For now, this is
all done in the `run` class level method. For Radish, the method has to:

* Archive data hourly for our [new historical graphs](https://img.skitch.com/20110705-gafpafq1sea7cj55hqcshb4qia.png)
* Activate accounts daily, which will prevent a reminder email from being sent out.

## The test

I ended up just using RSpec to test this out.
[Timecop](https://github.com/travisjeffery/timecop) helps with freezing time in
the right place, and [Bourne](https://github.com/thoughtbot/bourne) gives us
[test spies](https://thoughtbot.com/blog/spy-vs-spy) to make sure the right
methods get called. Here's the test I ended up with:

    require 'spec_helper'

    describe Cron do
      before do
        Account.stubs(:activate)
        Archive.stubs(:store)
      end

      let!(:project1) { Factory(:project) }
      let!(:project2) { Factory(:project) }

      after do
        Timecop.return
      end

      it "runs nightly" do
        Timecop.freeze(Time.now.midnight)
        Cron.run

        Account.should have_received(:activate)
        Archive.should_not have_received(:store)
      end

      it "runs hourly" do
        now = Time.now.midnight + 1.hour
        Timecop.freeze(now)
        Cron.run

        Account.should_not have_received(:activate)
        Archive.should have_received(:store).with(project1, now, now - 1.hour)
        Archive.should have_received(:store).with(project2, now, now - 1.hour)
      end
    end

Let's start from the top of the test here. The `before` block uses Bourne to
stub out the two class level methods on other models in the application, so we
can assert they were called later. We then hook up two `let!` blocks for two
projects, which we will use later. `let!` as opposed to `let` in RSpec will
force those blocks to be evaluated for each test run instead of being lazy
evaluated when they are referenced. Finally, since we're going to freeze time
for each test, we have to return to the system time in the `after` block.

Our two tests verify our daily and hourly scenarios. The first freezes time at
midnight, which may not be exactly when Heroku runs our cron job, but all we
care about is that the daily task runs only once. The hourly test freezes time
at 1:00AM and checks that *only* the hourly task gets run, and not the daily.

I could have gone a little more gung-ho on this test, perhaps running through an
entire 24 hours and making sure the daily task was only called once, but this
was good enough.

## The implementation

Here's what I ended up with in my `Cron` model:

    class Cron
      def self.run
        now = Time.now

        Project.find_each do |project|
          Archive.store(project, now, now - 1.hour)
        end

        if now.hour == 0
          Account.activate
        end
      end
    end

The implementation ended up to be pretty simple: grab the time, archive always
(since the task is run hourly), and if it's run in the 12:00AM hour, activate
accounts.

Pushing this code down into a model makes more sense now...too much code that
deals with models and not test data or factories in a Rake task always smells a
bit funky to me. It's also much easier to refactor the code now that we're in a
real model and we have a testing feedback loop in place. For instance, the
`Project.find_each` loop could easily be extracted into the `Project` class.

## Hacking your time zone

During this process I learned of a UNIX trick that can help with testing this
locally: the `TZ` flag. The appropriately named [UNIX Power
Tools](http://docstore.mik.ua/orelly/unix/upt/ch06_06.htm) puts it best:

> The TZ environment variable is a little obscure, but it can be very useful. It
> tells UNIX what time zone you're in.

Most of the time scripts will get this from your environment, but you can
override it. Here's a simple way to test this:

    % TZ=UTC+5 ruby -e "puts Time.now"
    2011-07-05 11:30:48 -0500

    % TZ=UTC-8 ruby -e "puts Time.now"
    2011-07-06 00:30:42 +0800

    % TZ=UTC ruby -e "puts Time.now"
    2011-07-05 16:30:53 +0000

So basically, if you want to force it to be midnight when running a test or from
a small script, you can use this environment variable to add/subtract time from
your current time zone.

## Less of a pain

Testing cron is now actually feasible, and now you can be assured your task will
work without waiting an entire hour or day to find out. Which of course, means
you can [ship it](http://yfrog.com/z/khl8uekj) faster!
