Thursday, September 04, 2008

Ruby: Recording Method Calls and Playback With Inject

Sometimes you want to call methods on an object, but you want to delay the actual execution of those methods till a later time.

For example, in expectations you create a mock at parse time, but you actually want the mock to be available at execution time.

class SystemProcess
def start
puts "started"
new StartedProcess
end
end

Expectations do
expect SystemProcess.new.to.receive(:start) do |process|
process.start
end
end

In the above code you define what you expect when the file is parsed, but you actually want the process expectation to be set when the do block is executed. This can be (and is) achieved by using a recorder to record all the method calls on the process object. At execution time the method calls are played back and the initialized process object is yielded to the block.

The code for a recorder is actually quite trivial in Ruby.

class Recorder
attr_reader :subject
def initialize(subject)
@subject = subject
end

def replay
method_stack.inject(subject) { |result, element| result.send element.first, *element.last }
end

def method_stack
@method_stack ||= []
end

def method_missing(sym, *args)
method_stack << [sym, args]
self
end
end

Here's an example usage of a recorder.

class SystemProcess
def start(in_seconds)
puts "starting in #{in_seconds}"
sleep in_seconds
StartedProcess.new
end
end

class StartedProcess
def pause(in_seconds)
puts "pausing in #{in_seconds}"
sleep in_seconds
PausedProcess.new
end
end

class PausedProcess
def stop(in_seconds)
puts "stopping in #{in_seconds}"
sleep in_seconds
self
end
end

Recorder.new(SystemProcess.new).start(1).pause(2).stop(3).replay
# >> starting in 1
# >> pausing in 2
# >> stopping in 3

The only thing worth noting is that by using inject you can use a method chain that returns different objects. Traditional versions of a recorder that I've seen often assume that all the methods should be called on the subject. I prefer the version that allows for object creation within the fluent interface. In practice, that's exactly what was needed for recording and playing back Mocha's expectation setting methods.
Post a Comment