Thursday, March 6, 2008

Unit Testing Ruby Scripts


I created a ruby script to accept a few parameters, validate the parameters, and abort, with message, if those parameters are invalid. I then ran into a problem when I wanted to unit test the script. The following abort_script.rb is used as an example:

abort "Usage: abort_script dirname filename" unless ARGV.size == 2 



First I tried to require the script file in the unit test. This doesn't work though since the ruby interpreter executes the script. Having just the two require statements in abort_script_test.rb produces the abort message.
require 'test/unit'
require 'lib/abort_script'

 
chuck$ ruby test/abort_script_test.rb
Usage: abort_script dirname filename

To test the script, as it exists, we have to shell out and call the script. Another problem surfaced when I tried this. The abort method sends the message to stderr instead of stdout. It turns out not to be very trivial to figure out how to get stderr from a sub-shell process. Eventually, google and I found two good resources that discuss handling stderr when executing an external program. Process Management and this Pasadena Ruby Bridgade blog entry called 6 Ways to Run Shell Commands in Ruby.


The method that worked for me was redirecting the shells stderr to stdout. The abort message can then be obtained via the returned stdout using the backquote (`) command.
require 'test/unit'
class AbortScriptTest < Test::Unit::TestCase
def test_parms
use_error = "Usage: abort_script dirname filename\n"

exit_status = `ruby lib/abort_script.rb 2>&1`
assert_equal(use_error, exit_status)
end
end

If you have a better way of unit testing ruby scripts (i.e. not classes) please leave a comment.

2 comments:

Stephan Wehner said...

You can use the Test::Unit with the mocha gem and expectations.

Here is a complete example :

require 'test/unit'
require 'rubygems'
require 'mocha'

class AbortingClass
def trigger_abort
abort 'No database name found'
end
end

require 'mocha'

class AbortingClassTest < Test::Unit::TestCase
def test_aborting
AbortingClass.any_instance.expects(:abort).with('No database name found')
AbortingClass.new.trigger_abort
end
end



$ ruby abort_example.rb
Loaded suite abort_example
Started
.
Finished in 0.001736 seconds.

1 tests, 1 assertions, 0 failures, 0 errors






(Cannot see how to handle source code with your blogging software, sorry)


rspec will allow the same.

Stephan

Stephan Wehner said...

Sorry, I overlooked your requirement of "no class".

Below script+test "with no class". But I think you'll have your abort triggered in different, likely easier-to-test contexts.

$ cat abort_script.rb
abort 'Script aborts!'
$ cat abort_script_test.rb
require 'test/unit'

require 'rubygems'
require 'mocha'

class AbortingClassTest < Test::Unit::TestCase
def test_aborting
Object.any_instance.expects(:abort).with('Script aborts!')
require 'abort_script'
end
end


$ ruby abort_script_test.rb
Loaded suite abort_script_test
Started
.
Finished in 0.002245 seconds.

1 tests, 1 assertions, 0 failures, 0 errors


Stephan