Mocking Backticks and Other Kernel Methods

There's a bunch of places in our build where we need to execute a system command (such as running sqlplus) but often times we've found that the command fails and our build happily churns away. It's pretty easy to check the result of system command with $?.success? but we have to remember to do that everywhere... So that means it's time to extract a method. Here's what Kurtis and I came up with:

module DatabaseHelper
def self.command_runner(command)
output = `#{command}`
puts output
fail unless $?.success?
fail if output.include? "ERROR"
output
end
end


So you pass in a command as a string and the command runner:
  • runs the command
  • prints out the output
  • fails if the return code is bad
  • fails if the output includes "ERROR" (useful when running database imports with sqlplus)
  • and returns the output just to be a good citizen

So that's cool but when I started to write the module I thought "Hey, I can test this." Which is a cool side effect of extracting methods from your rake file. So cool that you probably want to consider it even if the method won't be re-used.

Anyway, I started to write the test and realized that I would need to mock the backtick, which is a Kernel method. I'm using mocha so I tried:

Kernel.expects(:`)

and

Kernel.any_instance.expects(:`)

But no luck. Turns out you need to mock the class in which the backtick method will be called. So here's the tests I ended up with:

unit_tests do

test "command runner executes a command" do
DatabaseHelper.expects(:`).returns("some output")
$?.expects(:success?).returns(true)

output = DatabaseHelper.command_runner("command")
assert_equal "some output", output
end

test "command runner fails if ERROR in output" do
DatabaseHelper.expects(:`).returns("ERROR: this is broken")
$?.expects(:success?).returns(true)

assert_raises(RuntimeError) do
DatabaseHelper.command_runner("command")
end
end

test "command runner fails if process fails" do
DatabaseHelper.expects(:`)
$?.expects(:success?).returns(false)

assert_raises(RuntimeError) do
DatabaseHelper.command_runner("command")
end
end
end

I was having such a good time mocking that I later went back and mocked out the puts so I didn't have to see the output when running the tests. Gotta keep things clean.

Btw, you may notice that I'm not using Rspec a work anymore. Yep, it's true. My new team is using DUST (well, the code that would later be part of the inspiration for DUST) which is pretty cool. I still prefer Rspec, but I can't really make the case that we should switch to Rspec mid-stream (if we were using straight Rails testing, I would make that case.).

Comments

Tero Tilus said…
Thanks! Now I know where to start looking for a solution.

I've been trying to mock require and now I'm stuck with RSpec built in mocking not having an equivalent of #any_instance.
Don Petersen said…
Thanks a lot for posting this! You just saved me some serious frustration trying mock the backtick unsuccessfully. It's fanciness kind of threw me off, didn't even know where to start.
Anonymous said…
Maybe things have changed, but I was just able to mock Kernel.` without problems.

Kernel.expects( :` ).with( 'unzip -d /tmp test.zip' )

Passed in my tests. This is Rails 2.1.
Anonymous said…
Thanks for posting this! Very helpful!

Popular posts from this blog

Point Inside a Polygon in Ruby

What's a Good Flog Score?

SICP Wasn’t Written for You