Friday, July 13, 2007

Ruby VCR

Today, I was writing some tests and I needed to mock a server. Now, normally for mock objects, I use the FlexMock library. But for this one, it had a LOT of different input parameters and I didn't want to specify all of them with a corresponding result. I really just wanted to record the way the server actually responds for the calls; save the responses; and play them back in a mock object. So, I wrote a module to do just that.
module VCR
    module VCR::Parent
        # Watch every public method in this class
        # TODO: Question? Do I want to exclude Object's methods?
        def watch_all_public_methods
            o = Object.new
            for m in public_methods - o.methods
                unless ['record_method'].include?(m)
                    record_method(m)
                end
            end
        end
    end

    #
    # Creates a recording Module using the given filename.
    #
    def VCR::Record(filename)
        Module.new do
            extend_object(VCR::Parent)

            @@recording_filename = filename
            @@results = {}

            #Watches a method and records the result.
            def watch_method(symbol)
                self.class.send(:define_method, symbol, Proc.new {|*args|
                    result = super
                    @@results[[symbol, *args]] = result
                    File.open(@@recording_filename, 'w') do |f|
                        Marshal.dump(@@results, f)
                    end
                   result
               })
            end
        end
    end

    #
    # Creates a playback Module given the filename for saved data.
    #
    def VCR::Playback(filename)
        Module.new do
            extend_object(VCR::Parent)

            @@results = Marshal.load(File.open(filename))

            #watches a method for playback
            #when the given method is called, it will return the response
            #from the saved file.
            def watch_method(symbol)
                self.class.send(:define_method, symbol, Proc.new {|*args|
                                                    @@results[[symbol, *args]]
                                                    })
            end
        end
    end
end

So for example, Lets say I have a http server object that gets data from the internet.

class HttpServer
    def get_data(url) ... end
end

I can create a new server that will mimic that class.

class VCRServer < HttpServer
    include VCR::Record('save_filename.dat')
    def initialize(*args)
        super
        watch_method :get_data
    end
end

This will only record the method get_data. If we wanted to record everything, we
could have used:

watch_all_public_methods()
To swtich this Class to playback-mode, simple change 'Recorder' to 'Playback'

class VCRServer < HttpServer
    include VCR::Playback('save_filename.dat')
    def initialize(*args)
        super
        watch_method :get_data
    end
end

No comments: