Thursday, January 24, 2008

RSpec Delegation Assertion

So I've made the jump to using RSpec, and I love it. I really like the declarative nature of the matchers, and feel compelled to write useful ones. Below is a matcher for testing delegations. As of right now, I have not yet decided if I should have the
matcher be at the class level or the object level. We'll see. Perhaps iteration 2.


class Comment < ActiveRecord::Base
extend Forwardable
belongs_to :user
def_delegator :user, :name, :author_name
end

describe Comment do
it 'should delegate author_name to user' do
Comment.should delegate(:author_name, :to => :user, :via => :name)
end
end

Below is a helpful tester for delegations, inspired by Jay Fields, and more importantly made possible by Obie Fernandez's most excellent The Rails Way.

module Spec
module Rails
module Matchers
# describe Comment do
# it 'should delegate author_name to user' do
# Comment.should delegate(:author_name, :to => :user, :via => :name)
# end
# end
class Delegation
private
attr_reader :expected, :actual, :method_name, :options, :to, :via
public
def initialize(method_name, options = {})
@method_name = method_name
options.symbolize_keys!
@to = options[:to]
@via = options[:via] || method_name
end

def matches?(klass)
@actual = klass
delegate_class = Class.new
delegate_class.send(:attr_accessor, @via)

delegate_object = delegate_class.new
delegate_object.send("#{@via}=", :spec_rails_matchers_delegation)

@base_object = @actual.new

# Use the singleton class of the base_object
# I am rewriting the @to method on the base_object's meta_class
# but not altering the base_object's parent class
# So if I instantiate another instance of @actual
# it will not have the below behavior, only the @base_object will
#
# http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
singleton = @base_object.instance_eval("class << self; self; end")
singleton.class_eval("attr_accessor :#{@to}")

@base_object.send("#{@to}=", delegate_object)
@base_object.send(method_name) == :spec_rails_matchers_delegation
end

def failure_message
"expected #{actual.to_s} to delegate :#{method_name} to :#{to} via :#{via}"
end

def negative_failure_message
"expected #{actual.to_s} to NOT delegate :#{method_name} to :#{to} via :#{via}"
end

def to_s
"#{actual.to_s} delegates :#{method_name} to :#{to} via :#{via}"
end

end
def delegates(method_name, options = {})
Delegation.new(method_name, options)
end
alias_method :delegate, :delegates
end
end
end