Stubbing Ruby

Posted by scientific on December 15, 2006


Ruby is a great language for doing things in unexpected ways. I had such an occasion recently, where I wanted the functionality of a child object but without the hassle and time of setting one up. Plus, I wanted the data to be delivered from the parent object, so I didn’t want to worry about managing/syncing context or binding between the parent and the child..

So I spent three times as long building this “Stubber” class which can be used to generate arbitrary “child-like” behaviors. You can think of Stubber like Struct on steroids. Rather than just giving you access to variables in a structure, it lets you assign any Ruby code to the structure. Of course this can get really weird if you misuse it, but show me something in Ruby where that’s not the case. In the example attached, we create a method called “constraint” and we use Stubber class to “stub out” two method calls to this method. So the caller can say:

obj.constraint.to_sql

But Stubber will actually allow us to “point” this to any arbitrary Ruby code that we want. In the example below we redirect this call to “self.item” which in this context means that these two call are identical:

obj.constraint.to_sql
obj.item

But you can use Stubber for any related purpose. I haven’t looked very hard to see if Stubber is thread-safe but my instinct tells me that @cur_binding class variable makes Stubber NOT thread-safe, so be aware of that if you do more than just play with this neat toy.

Stubber class itself:

# note Stubber is probably not thread-safe..
class Stubber
  # create an initial stub on create
  def initialize(stub_method_name, actual_method_code)
    @stubs = Hash.new
    self.add_stub(stub_method_name, actual_method_code)
  end
  # add additional stubs as desired
  def add_stub(stub_method_name, actual_method_code)
    @stubs[stub_method_name.to_sym] = actual_method_code
  end
  # call this to “execute” stubber (which really sets the current binding
  # and passes itself to the caller)
  def stub(b)
    @cur_binding = b
    return self
  end
  def method_missing(symbol, *args)
    stub_code = @stubs[symbol.to_sym]
    if stub_code
      eval(stub_code, @cur_binding)
    else
      super(symbol, *args)
    end
  end
end

Example code that makes use of Stubber:

class StubMe
    def initialize
    @stubber = Stubber.new(:to_sql, "self.item")
    @stubber.add_stub(:hello_world, "'Hello World the hard way!'")
  end

  def item
    "Hello world"
  end

  def constraint
    # here's the main call that "activates" stubber
    @stubber.stub(binding)
  end
end

stub_this = StubMe.new

#this returns self.item, via a call to Stubber
puts stub_this.constraint.to_sql

#returns "Hello World the hard way!" via Stubber
puts stub_this.constraint.hello_world

#correctly raises exception when method not found by Stubber
puts stub_this.constraint.goobly_gook
Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

Comments