Testable Ruby Scripts
Fri, Mar 25, 2011One 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.