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; | |
} |
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; | |
} | |
} |
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 |
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 |
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.