Testable Ruby Scripts

Fri, Mar 25, 2011

One of the great beauties of the Ruby language is how easy it is to be productive.

When you are writing Ruby code you can concentrate on the behavior you need to implement rather than the intricacies of the language you are using. This also means Ruby is a great scripting language. Its easy to knock up a quick & dirty doit.rb script whenever you need to.

However, if you need to maintain those scripts over time, assuming they are non-trivial, you will want to put in just a tiny amount of structure in order to allow your scripts to be testable in the console, or with a formal test harness.

Thankfully, there is an easy pattern you can follow to make your Ruby scripts testable.

A Simple Ruby Script

For the purposes of this article, lets imagine a really dumb script that can convert miles to kilometers for us:

m2km.rb

  #!/usr/bin/env ruby

  KM_PER_MILE = 1.609344

  ARGV.each do |miles|
    km    = miles.to_f * KM_PER_MILE       # this is where the work is done
    puts "#{miles} miles = #{km} km"
  end

(Lets also imagine that, for clarity, it leaves out any error handling!)

So that…

m2km 10
10.0 miles = 16.09344 km

Embrace your Framework

While I want to ultimately talk about making any standalone script testable, I am going to hold off on that for one moment to consider scripts used within an existing framework.

If you are working in a framework like Ruby on Rails then it would be natural to move the code that does the important work into its own class in the /lib or /vendor directory and strip the m2km.rb script down to the bare bones:

/lib/conversion.rb

  module Conversion
    KM_PER_MILE = 1.609344

    def self.m2km(miles)
      miles.to_f * KM_PER_MILE
    end
  end

/scripts/m2km.rb

  #!/usr/bin/env ruby
  require 'conversion'

  ARGV.each do |miles|
    puts "#{miles} miles = #{Conversion.m2km(miles)} km"
  end

Obviously, in this trivial example, that seems overkill, but if the conversion functionality was much more complex this would now allow you to write unit tests for the Conversion class using Test::Unit or some other testing library.

/test/unit/conversion_test.rb

  class ConversionTest < Test::Unit::TestCase

    def test_m2km
      assert_equal(0,        Conversion.m2km(0))
      assert_equal(1.609344, Conversion.m2km(1))
      assert_equal(1.609344, Conversion.m2km(1.0))
      # etc
    end

  end 

or to experiment within IRB:

irb -r conversion
> Conversion.m2km(1)
=> 1.609344

Testable Standalone Scripts

But what about a standalone script ? How do you make that testable ? How do you experiment with it in IRB ?

You could follow the same pattern as before and break the file into multiple pieces, but if you want to keep the script self contained, another approach is to define the helper Class/Module in the script itself and setup the script so that it can be loaded into IRB or a test harness without running:

m2km.rb

  #!/usr/bin/env ruby

  module Conversion
    KM_PER_MILE = 1.609344
    def self.m2km(miles)
      miles.to_f * KM_PER_MILE
    end
  end

  if $0 == __FILE__      # let script load in irb/test without running it
    ARGV.each do |miles|
      puts "#{miles} miles = #{Conversion.m2km(miles)} km"
    end
  end

The Important Part

The trick to this working is the following condition:

  if $0 == __FILE__
    # scripty bits
  end

This works because the $0 global variable contains the name of the file run from the command line, so that when your script is run directly, the scripty bits will run, but when your script is simply ‘required’ inside IRB or a test harness this condition will not be true and the scripty bits will not run.

Voila! Now you have a script that can be run from the command line, or loaded into IRB or a test harness as needed.