Tuesday, September 12, 2006

Ruby Stub Variations: OpenStruct

Ruby Stub Variations: Introduction
Ruby Stub Variations: OpenStruct
Ruby Stub Variations: TestStub
Ruby Stub Variations: Struct
Ruby Stub Variations: Stubba


Shortly after I begin to program full time using Ruby I was exposed to OpenStruct. I was quite pleased with the simplicty OpenStruct offered. OpenStruct allows you to create objects with their attributes initialized. For example:
require 'ostruct'
record = OpenStruct.new(:name=>'jay')
puts record.name # -> jay
OpenStruct provided value by allowing me to easily create stubs in one line of code. The code below represents a more robust version of the example test. OpenStruct helps reduce the fragility of the test by removing the dependency on the Select class.
def test_values_are_appened_to_insert_statement
statement = Insert.into[:table_name].values do
OpenStruct.new(:to_sql=>'select column1, column2 from table2')
end
assert_equal "insert into table_name select column1, column2 from table2", statement.to_sql
end
Despite the value of OpenStruct, it also has some limitations. The first issue we encountered was that it did not respond as expected when the OpenStruct instance was frozen.

Another issue with OpenStruct is that it does not respond as expected if you specify a value for an already defined method. My team noticed this behavior while using an OpenStruct instance in place of a ActiveRecord::Base subclass instance. The test required the OpenStruct instance to respond to id; however, the number that was returned did not match the value passed in the constructor. The following failing test should demonstrate the described issue.
require 'test/unit'
require 'ostruct'

class OpenStructTest < Test::Unit::TestCase
def test_id
assert_equal 2, OpenStruct.new(:id=>2).id
end
end
This issue stems from the current implementation of OpenStruct. OpenStruct stores it's attribute values in a Hash. When you attempt to access an attribute the call is actually delegated to method_missing. OpenStruct's implementation of method_missing returns the value from the Hash if it finds a matching key, otherwise nil. Unfortunately, method_missing will never be called if a method, such as id, is previously defined.

3 comments:

  1. Anonymous12:59 PM

    You may want to look at Builder::BlankSlate.
    It manages to hide all method names, an idea like this could allow you to make a better OpenStruct for your own use.

    ReplyDelete
  2. Jay, I ran into this issue today and after some digging I figured out you can simply access the desired values out of the internal hash. So to get the right id value in your example, you'd have to add a method such as

    def id
    @table[:id]
    end

    ReplyDelete
  3. I find the interface not quite uniform:

    $ irb
    irb(main):001:0> shallow_hash = {:first => 'a', :second => 'b'}

    => {:second=>"b", :first=>"a"}

    irb(main):002:0> deep_hash = {:first => {:foo => 'bar1'}, :second => {:foo => 'bar2'}}

    => {:second=>{:foo=>"bar2"}, :first=>{:foo=>"bar1"}}

    irb(main):003:0> require 'ostruct'

    => true

    irb(main):005:0> shallow_ostruct = OpenStruct.new(shallow_hash)

    => #< OpenStruct second="b", first="a">
    irb(main):007:0> shallow_ostruct.first
    => "a"

    irb(main):008:0> shallow_ostruct.first.class

    => String

    irb(main):009:0> deep_ostruct = OpenStruct.new(deep_hash)

    => #< OpenStruct second={:foo=>"bar2"}, first={:foo=>"bar1"}>

    irb(main):011:0> deep_ostruct.first
    => {:foo=>"bar1"}

    irb(main):012:0> deep_ostruct.first.class

    => Hash

    Not sure if this is salvagable.

    Stephan

    ReplyDelete

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