In this tutorial, we explore a powerful Ruby class called Recorder
. This class wraps any Ruby object to record all method calls made to it. This functionality is invaluable for generating mocks, aiding in introspection, and creating detailed documentation.
The Recorder
class operates by overriding the method_missing
method to intercept calls to its target object. We’ve also included the respond_to_missing?
method to ensure that respond_to?
works correctly with dynamically handled methods:
require 'time'
class Recorder
def initialize(target_object)
@object = target_object
@history = []
end
def method_missing(method_name, *args, &block)
if @object.respond_to?(method_name)
start_time = Time.now
result = @object.public_send(method_name, *args, &block)
end_time = Time.now
@history << { method: method_name, args: args, result: result, called_at: start_time, returned_at: end_time }
result
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
@object.respond_to?(method_name, include_private) || super
end
def _history
@history.dup
end
end
Lambdas are used to create test expectations based on the recorded history, compatible with both RSpec and Mocha:
# Lambda for generating RSpec expectations
rspec = ->entry {
args = entry[:args].map(&:inspect).join(", ")
result = entry[:result].inspect
"expect(object).to receive(:#{entry[:method]}).with(#{args}).and_return(#{result})"
}
# Lambda for generating Mocha expectations
mocha = ->entry {
args = entry[:args].map(&:inspect).join(", ")
result = entry[:result].inspect
"object.expects(:#{entry[:method]}).with(#{args}).returns(#{result})"
}
Using pretty-printing to provide a clear overview of interactions:
require "pp"
require "stringio"
# Helper for pretty-printing objects
pretty = ->object {
sio = StringIO.new
PP.pp(object, sio)
sio.string
}
# Comment each line of the pretty-printed result
comment = ->string {
string.lines.map { |line| "# " + line }.join("\n")
}
# Generate detailed method interaction including the result
method = ->entry {
args = entry[:args].map(&:inspect).join(", ")
"obj.#{entry[:method]}(#{args})" + "\n" +
entry[:result].then(&pretty >> comment)
}
Here’s how the Recorder
class can be used with a simple string object:
string_proxy = Recorder.new("Hello")
string_proxy.upcase!
puts string_proxy._history.map(&rspec)
# expect(object).to receive(:upcase!).with().and_return("HELLO")
puts string_proxy._history.map(&mocha)
# object.expects(:upcase!).with().returns("HELLO")
puts string_proxy._history.map(&method)
# obj.upcase!()
# # "HELLO"
This setup effectively tracks and analyzes method calls, proving to be an invaluable tool for debugging and documenting the behavior of complex systems.