Saturday, July 29, 2006

My Ruby Stub

Being new to Ruby, one of the first things I wanted to do was figure out how to apply the testing style I am comfortable with in Java.  Several years ago, Bob Lee and I wrote an IDEA plugin to generate stub-object source files from interface definitions.  For a given interface:

public interface Foo {
Bar makeABar(String hey, List now) throws BarClosedException;
}
view raw snippet.java hosted with ❤ by GitHub

The stub generator would create a class that looks like:

public class StubFoo implements Foo {
public boolean makeABarCalled;
public String makeABarHey;
public List makeABarNow;
public Bar makeABarReturn;
public BarClosedException makeABarException;
public Bar makeABar(String hey, List now) {
makeABarCalled = true;
makeABarHey = hey;
makeABarNow = now;
if (makeABarException != null) {
throw makeABarException;
}
return makeABarReturn;
}
}
view raw snippet.java hosted with ❤ by GitHub

The generated stubs are useful in testing because I can set them up with a return value or exception to simulate some condition that I am trying to test. The methods of the stub keep track of whether they were called and any parameters that were passed to them. This approach follows the state-based testing that Fowler describes, and is central to my own programming style.

In Ruby, the same kind of thing can be accomplished at runtime in a very dynamic way. This unit test demonstrates the kind of thing I am looking for in a stub generator:

require 'stub'
class Foo
def bar(hey, now)
end
def hoy()
end
end
class Bar < Foo
def blap(howdy)
end
end
class TestStub < Test::Unit::TestCase
def test_declared_methods
stub = Stub.new(Foo)
stub.bar_return = 'heynow'
result = stub.bar('baba', 'ganush')
assert stub.bar_called?
assert_equal false, stub.hoy_called?
assert_equal 'baba', stub.bar_params[0]
assert_equal 'ganush', stub.bar_params[1]
assert_equal 'heynow', result
end
def test_equals_method
one = Stub.new(Foo)
two = Stub.new(Foo)
assert_equal one, two
one.bar_return = 'yeah'
two.bar_return = 'yeah'
assert_equal one, two
two.bar_return = 'foo'
assert_not_equal one, two
assert_not_equal one, Stub.new(Fixnum)
end
def test_stub_throws_supplied_error
stub = Stub.new(Foo)
stub.bar_error = StandardError.new
assert_raise(StandardError) { stub.bar('hey', 'now') }
end
def test_stub_inherited_methods
stub = Stub.new(Bar, true)
stub.blap_return = 'one'
stub.hoy_return = 'two'
blap_result = stub.blap
hoy_result = stub.hoy
assert stub.blap_called?
assert stub.hoy_called?
assert_equal 'one', blap_result
assert_equal 'two', hoy_result
end
def test_stub_no_inherited_methods_by_default
assert_raise(NoMethodError) { Stub.new(Bar).hoy }
end
def test_params_method_created_when_needed
stub = Stub.new(Foo)
assert_nothing_raised do
stub.bar('a', 'b')
stub.bar_params[0]
end
stub.hoy
assert_raise(NoMethodError) { stub.hoy_params[0] }
end
end
view raw snippet.rb hosted with ❤ by GitHub

The Ruby Stub implementation was tricky to figure out until I learned how to use define_method:

class Stub
def initialize(clazz, include_super = false)
@clazz = clazz
meta = class << self; self; end
methods = clazz.public_instance_methods(include_super)
@called_table = Hash.new(false)
@returns_table = Hash.new()
@errors_table = Hash.new()
methods.each do |method_name|
add_reader(meta, "#{method_name}_called?") {
@called_table[method_name]
}
add_writer(meta, "#{method_name}_return") { |value|
@returns_table[method_name] = value
}
add_writer(meta, "#{method_name}_error") { |error|
@errors_table[method_name] = error
}
add_reader(meta, method_name) { |*args|
@called_table[method_name] = true
add_reader(meta, "#{method_name}_params") { args } unless args.empty?
raise @errors_table[method_name] unless @errors_table[method_name].nil?
@returns_table[method_name]
}
end
end
def ==(other)
return false unless(other.kind_of?(Stub))
return false unless(@clazz == other.clazz)
@returns_table == other.returns_table
end
private
def add_reader(meta, method_name, &block)
meta.send(:define_method, method_name.to_sym, block)
end
def add_writer(meta, item, &block)
meta.send(:define_method, "#{item}=".to_sym, block)
end
attr_reader :clazz, :returns_table, :errors_table
protected :clazz, :returns_table, :errors_table
end
view raw snippet.rb hosted with ❤ by GitHub

So in my first real foray into a Ruby utility, I was able to create something that does what I need and learn a little about the API in the process. Of course, after savoring the feeling of learning and having written something that I thought was cool, I had a closer look the Stubba API from the Mocha library and realized that I still had a long way to go. Still, the exercise was a useful one; the more I use Ruby the more I really like it.