A Ruby Test Framework in One Line

11:17 AM EDT Friday, May 23 2008

Time for a little Friday fun with Ruby. I love Ruby for things like this. I realize this has little practical usage, but the fact that it's possible says a lot about the language. You can build a test "framework" in one line of code in Ruby. Let's say we are going to write a method that take 0 to n arguments and adds them up. So following TDD, we write the tests first:

tests = {
  "sum" => 0,
  "sum(7)" => 7,
  "sum(2, 3)" => 5,
  "sum(2, 3, 5)" => 10
}

The tests are defined in a hash where the key is the code to be tested in a string and the value is the expected result. Next we write our implementation:

def sum(*args)
  (args || 0).inject(0){|s,e| s+e}
end

And finally, we create and execute our test framework in one line of code:

tests.each{|e,v| puts((r=eval(e))==v ? ". #{e}" : "! #{e} was '#{r}', expected '#{v}'")}

The output looks like this:

. sum
. sum(2, 3, 5)
. sum(7)
. sum(2, 3)

If we change one of our tests to show a failure, you get this:

. sum
. sum(2, 3, 5)
. sum(7)
! sum(2, 3) was '5', expected '8'

Posted in  | Tags Ruby

Comments Feed

1. cute! I like the simplicity of the runner.

But your impl looks flawed.... shouldn't it be:

args.inject(0) {|s,e| s+e}

the || 0 part would only help you if you explicitly passed in nil or false. You might as well blow up if they're passing in bad data.

# Posted By Ryan Davis on Friday, May 23 2008 at 07:19 EDT

2. @Ryan

Good point, for some reason I was thinking that passing no args would mean that args would be nil, but it is an empty array. I think this is the right implementation:

args.inject(0){|s,e| s+e.to_i}

Which allows sum(nil, "1", 2) to return 3, which of course I was easily able to add a test for. Yay for TDD!

# Posted By Paul Barry on Friday, May 23 2008 at 09:09 EDT

3. Nice one-liner :-)

I've been inspired by this and built a more robust one, but it's 20 lines long :P

def MTest(tests)
if tests[0].is_a?(String) then puts tests.shift end
threads = []
results = {:pass => 0, :fail => 0, :err => 0}
tests.each do |t|
e,p,v = *t
threads << Thread.new do
begin
puts(if (r = p.call) == v
results[:pass] += 1
". #{e}"
else
results[:fail] += 1
"! #{e} was #{r}, expected #{v}"
end)
rescue => x
results[:err] += 1
puts "@ #{x.class} at line #{x.backtrace[0].split(':')[1]}"
end
end
end
threads.join
results
end

This allows you many tricks like naming your tests, don't abort on exceptions but show a notice and get a summary for your tests.

# Posted By François Vaux on Friday, May 30 2008 at 05:23 EDT

4. I forgot to notice that the test format is now as follows :

tests = [
&nbsp;&nbsp;"= Some Basic Tests =",
&nbsp;&nbsp;['true', proc { true }, true],
&nbsp;&nbsp;['true', proc { false }, true],
&nbsp;&nbsp;['sum(2,5)', proc { sum(2,5) }, 7]
]

And you just have to call MTest(tests)

(sorry for screwing the indent in my last post)

# Posted By François Vaux on Friday, May 30 2008 at 05:25 EDT

5. outstanding!

# Posted By Kerry Gunther on Tuesday, August 05 2008 at 03:55 EDT

Add a Comment