On 4/2/07, Frederick Cheung <fred at 82ask.com> wrote:
>>> On 2 Apr 2007, at 17:24, James Moore wrote:
>> >
> > I'm now wondering whether a better way to do this is to add a
> > facility to
> > add callbacks that can create mocks and stubs when a particular
> > object is
> > instantiated from the database. A sort of cleanup method on Rails
> > find*()
> > methods that will allow you to do mock/stub methods on an object
> > with a
> > particular id (or some set of criteria) when it's created.
>> I did this via the after_find callback a few weeks ago. I posted my
> code here when I did it maybe you'll find some inspiration there ?
>> Fred
>
Thanks Fred - I took a look, and here's what I'm currently playing with.
First, I tried doing just a mock of the
WhateverRecord.expects(:find).with(32).returns(my_mocked_obj).
The problem there was that Rails doesn't always just do a find with an id;
when you've got relations and validations, you'll get finds that specify
more than just the id itself.
What does get called every time, with the things you want arranged in a nice
convenient package, is #instantiate. It's the method that turns the data
for the row into an object, and it's primary purpose in life is to support
STI. So I hijacked it, and now am using something that looks like this:
Annotation.send :include, MockActiveRecord
foo = Foo.new :whatever => 'blah'
foo.expects(:snark).with(999)
Foo.mock_active_record(foo.id => foo)
Every time a record of class Foo is created, the id is checked to see if it
matches an id passed to mock_active_record(). If there's a match,
that object is returned, otherwise the normal #instantiate is called to
build a new object.
Here's the current (experimental) code:
# Adds mock_active_record to an ActiveRecord class.
#
# Any time an ActiveRecord object is instantiated,
# its id will be checked against a list of mocked
# ids. If the new object is present in that list,
# use the mock instead of the object produced
# by ActiveRecord.
#
# Example:
#
# class FooTest < Test::Unit::TestCase
# def test_foo
# Foo.send :include, MockActiveRecord
#
# # Create a new Foo in the database
# f = Foo.create :msg => 'blah'
#
# # Add a stub that will be used
# # in place of the Foo that was just
# # created.
# Foo.mock_active_record(f.id => stub(:msg => :abc))
#
# # find() will match the stub instead
# # of the record from the database.
#
# f = Foo.find(f.id)
# assert_equal :abc, f.msg
# end
# end
module InterceptMethod
def self.included target
target.extend ClassMethods
end
module ClassMethods
def intercept_method call_name, &block
unbound_method = instance_method call_name
define_method call_name do |*args|
bound_method = unbound_method.bind(self)
block.call(bound_method, *args)
end
end
def intercept_class_method call_name, &block
class_method = method call_name
s = class << self; self; end
s.send :define_method, call_name do |*args|
block.call(class_method, *args)
end
end
end
end
module MockActiveRecord
# Classes that include MockActiveRecord
# get:
#
# - an overridden instantiate()
# - mock_active_record()
def self.included target
target.class_eval do
include InterceptMethod
extend ClassMethods
intercept_class_method :instantiate do |method_obj, *args|
mocked_record_match?(*args) || method_obj.call(*args)
end
end
end
module ClassMethods
def mock_active_record ids_to_mocks
ids_to_mocks.each_pair do |k, v|
mocked_record_store k, v
end
end
protected
def mocked_record_match? record
@@mocked_records ||= {}
return @@mocked_records[record['id'].to_i]
end
def mocked_record_store k, v
@@mocked_records ||= {}
@@mocked_records[k.to_i] = v
end
end
end