Tuesday, September 26, 2006

Test::Unit test creation

Creating a unit test in Ruby is very similar to Java: create a method that begins with test_. In a recent discussion with some colleagues, Zak Tamsen and Muness Alrubaie, Zak described his distaste for this common convention. He continued by pointing out that using .NET Attributes was a superior approach for identifying tests.

This began a discussion on what a superior approach to writing tests could be when using Ruby. We implemented several examples before deciding on our preference.
test
def add_returns_the_sum_of_two_numbers
assert_equal 4, add(2, 2)
end

test 'add returns the sum of two numbers'
assert_equal 4 do
add(2,2)
end
Each implementation contained limitations. In the end we chose to go with a very simple implementation that is similar to how RSpec defines specifications.
test 'add returns the sum of two numbers' do
assert_equal 4, add(2, 2)
end
To accommodate this syntax we added the test class method to Test::Unit::TestCase.
class << Test::Unit::TestCase
def test(name, &block)
test_name = :"test_#{name.gsub(' ','_')}"
raise ArgumentError, "#{test_name} is already defined" if self.instance_methods.include? test_name.to_s
define_method test_name, &block
end
end
An additional benefit we were able to include in our change was the ability to raise an error when a test was defined with a duplicate name (or description in our case).

However, moving to this new style of test definition can cause issues. The first noticeable issue was that TextMate would no longer run individual tests. We quickly fixed this issue by looking at the code in the Ruby Bundle. The fix was to change the regex that selects the method you are testing. The code below shows how you can capture either the previous call to the test class method or the previously defined test method.
File.open(ENV['TM_FILEPATH']) do |f|
f.read.split("\n")[0...n].reverse.each do |line|
if line =~ /^\s*test\s+"([_a-z][_a-z0-9 ]*[\?!]?)"\s+do\s*$/i
print "test_#{$1.gsub(' ', '_')}"
break
elsif line =~ /^\s*def ([_a-z][_a-z0-9]*[\?!]?)/i
print $1
break
end
end
end
We also created a TextMate snippet in the Ruby Bundle to help us create tests in our new style. The following code is the snippet for easily creating tests.
test "${1:test name}" do
$0
end
After these quick changes we are quite happy working with our new format for creating tests.

7 comments:

  1. Why not just use rspec then?

    ReplyDelete
  2. Hmmm... interesting. This might have been a nice choice:


    test 'add returns the sum of two numbers'
    def test01
    assert_equal 4 do
    add(2,2)
    end
    end


    The method name could be anything, but would act as a unique tag, as opposed to the description.

    ReplyDelete
  3. Was there a particular reasoning as to why aspects where better?

    ReplyDelete
  4. @Pat, Using RSpect would actually be my preference, but it doesn't currently work well with Mocha.

    @tea42, We also considered that option, but didn't like the idea of arbitrary test names. We thought about generating the test names also, but never found a syntax that seemed superior.

    @Dan, I don't understand the question, sorry.

    ReplyDelete
  5. Jay, what problems do you get using RSpec with Mocha?

    ReplyDelete
  6. @Pat, I haven't tried since there are known issues that James Mead is working on. I'm on both the RSpec & Mocha mailing lists and both camps are interested in working together. I expect the issues to be resolved very soon. At which time I expect to write about how wonderful RSpect is. ;)

    ReplyDelete
  7. Anonymous2:21 PM

    re: rspec and mocha. What's happening in the short term is that rspec is enhancing it's own mocking framework to support class level mocking/stubbing. Expect that in a release in the next week or so.

    At the same time, we're working out a plugin model that any framework can use to plug itself into rspec. That should be available soon as well, at which point you'll be able to chose between rspec's built in mocking/stubbing and mocha/stubba, flexmock, et al. Even if those frameworks don't ship w/ the plug needed to plug into rspec, it should be easy enough for you to write your own connector at that point (and/or contribute one to the framework of your choosing).

    Of course, all of that is theoretical at this point ;)

    ReplyDelete

Note: Only a member of this blog may post a comment.