Got bit by the Typo bug, then got bit by Typo, so I changed to WordPress (I still love you, Rails). I’m starting this blog off with a little re-cap of my Ruby and Rails flails over the last few months.
I’m infected with Ruby/Rails. It’s a disease. Every conversation I have with someone about technology, programming, and the Web ends up coming back to Ruby and Rails. My friends are sick of me, seriously. It’s not my fault, I just have to tell people the Truth, and when I do, it ends up referring back to Ruby and Rails.
My friends in strong-typed land, please give me one chance to change your mind with a little story about Ruby. Ruby has an (okay) unit testing library called Test::Unit that is now part of the standard Ruby library. It has the features you’d expect of a unit testing library, like defining test suites, running test methods, making assertions and outputting little dots to the console as each test passes. I usually do all my testing from the console, whether I’m using nunit-console.exe or Test::Unit scripts in Rails. I find that I get much more rapid feedback about my tests and I have long ago let go of my need to literally see the red/green lights to iterate my red-green-refactor cycle.
One pet peeve of my (I have few) is when either my code or (worse) someone else’s code is writing out to the console during my unit tests and messing up my beautiful uninterrupted string of dots. So I’m working on some console-style scripts to augment a Rails application I’ve started recently (don’t worry, once they’re something to blog about, it will be here), and like a good tester bee, I want all of my features to be tested. So I whipped up some integration tests that would flex out my console tools, replete with OptionsParser testing to make sure all of my console options were covered. Looked great. So I ran my first tests, which just used the “backtick” method to invoke a console command from the Kernel. Which looks something like this in Ruby code:
`myscript --option-one -a=abc`
and will run that text verbatim at a new shell for your OS (bash for me). My tests passed and things were groovy. Then I checked my code coverage report from RCov (I’ll post later on how I use RCov in my Rails work), and found that my console tools weren’t getting covered by RCov. They’re forked off in new processes by the backtick method, and RCov isn’t going to instrument them there. D’oh!
At this point I hadn’t lost faith, so i decided to use the load method for my scripts:
load 'path/to/myscript.rb'
but I was lacking a way to pass my command-line arguments, and I ended up just making a hacky global variable for them. So I run my tests again and see to my horror that all of the console junk I was writing out in my console scripts was showing up in my test results (my beautiful linear dots!)
Now I’m getting serious. I’ve got coverage, but I’ve also got this mess. So I start thinking about Ruby and how it’s supposed to let you do anything you want. If I want to redirect the console output for the duration of a test method, I should be able to do that, right? Well, the answer I came up with might seem a little extreme, but it worked wonderfully, and I also have some code to share:
def execute_console_tool(consoleTool, arguments)
# redefine puts
Kernel::module_eval(<
@@output = ''
def output
@@output
end
def puts(*args)
args.each do |arg|
@@output += arg.to_s + "\n"
end if (args != nil)
end
end_of_eval
)
# set the command-line arguments and run
$commandLineArguments = arguments
load consoleTool
Kernel::output
ensure
# make sure puts is always put back
Kernel::module_eval(<
remove_method :puts
define_method(:puts) { |*args|
@@old_puts.call(*args)
}
end_of_eval
)
end
I dropped this method into my Rails test_helper.rb class, and then I can call it from my unit tests:
execute_console_tool('path/to/myscript', ['--version', '-a=b'])
How sweet is that? Okay, I admit, this isn’t really “elegant” in the sense that most Ruby code is elegant. But what I want to illustrate here is that Ruby is truly dynamic and even the “built-in” features are malleable at run-time. This is impossible in a statically typed language (or at minimum would require an enormous array of hacks, much more than this code).