Thursday, June 15, 2006

Ruby TestStub

Because I believe in testing one concrete class at a time I often make use of stubs. When I first began programming in ruby I would create stubs within my test classes
class SqlGeneratorTest < Test::Unit::TestCase
class StubIdGenerator
def nextval; 1; end
end

def test_something
stub = StubIdGenerator.new
#some logic
end
end
Then, as I previously blogged about, I started using classes as stubs. But, somewhere along the way I had forgotten about OpenStruct. It wasn't until recently when a co-worker suggested he was going to write a 'stub mother' that I remembered OpenStruct.

OpenStruct, as the documentation states, allows you to create data objects and set arbitrary attributes. The above StubIdGenerator definition and instantiation code can be replaced by using OpenStruct with one line.
def test_something
stub = OpenStruct.new(:nextval=>1)
end
Unfortunately, OpenStruct does not quite behave as I would expect a stub to. For example, you could write:
def test_something
stub = OpenStruct.new(:nextval=>1)
stub.nextval = 2
stub.nextval # nextval now returns 2 not 1.
end
To remedy this, I stole some of the behavior of OpenStruct and created TestStub. TestStub behaves the way I expect a stub to behave. It does not throw an exception when I call a writer that I've defined as a valid writer and returns a constant value when I call a reader. These requirements can be expressed as tests:
class TestStubTest < Test::Unit::TestCase
def test_writers_are_created_correctly
stub = TestStub.new(:bar, :baz)
assert_nothing_raised do
stub.bar = 2
stub.baz = 3
end
end

def test_calling_an_invalid_writer_raises_nme
stub = TestStub.new(:bar, :baz)
assert_raise(NoMethodError) { stub.cat = 4 }
end

def test_readers_are_created_correctly
stub = TestStub.new(:bar=>1)
assert_equal 1, stub.bar
end

def test_first_reader_when_multiple_readers_are_created_correctly
stub = TestStub.new(:bar=>1, :baz=>2)
assert_equal 1, stub.bar
end

def test_second_reader_when_multiple_readers_are_created_correctly
stub = TestStub.new(:bar=>1, :baz=>2)
assert_equal 2, stub.baz
end

def test_writers_when_readers_are_specified
stub = TestStub.new(:bar, :baz, :cat=>1)
assert_nothing_raised do
stub.bar = 2
stub.baz = 3
end
end

def test_readers_when_writers_are_specified
stub = TestStub.new(:cat, :dog, :bar=>1)
assert_equal 1, stub.bar
end
end
Since I was able to steal much of the behavior I needed from OpenStruct, the TestStub class was quite easy to throw together.
class TestStub
def initialize(*should_respond_to)
@table = {}
should_respond_to.each do |item|
meta = class << self; self; end
create_readers(meta, item) and next if item.kind_of? Hash
create_writer(meta, item) and next if item.kind_of? Symbol
end
end

def create_readers(meta, item)
item.each_pair do |key, val|
@table[key.to_sym] = val
meta.send(:define_method, key.to_sym) { @table[key.to_sym] }
end
end

def create_writer(meta, item)
meta.send(:define_method, :"#{item}=") { }
end

attr_reader :table # :nodoc:
protected :table

#
# Compare this object and +other+ for equality.
#
def ==(other)
return false unless(other.kind_of?(TestStub))
return @table == other.table
end
end

No comments:

Post a Comment

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