Thursday, July 12, 2007

Overriding def_delegator

In the project I work on, I make heavy use of the def_delegator method (yeah composition). One of the intrinsic problems of the delegation, is that my tests can get rather convoluted (and tightly coupled to implementation). I'm sure I could look into mock methods, but I needed an immediate solution.

Enter the ConditionallyOverrideDelegation module. This is a rails specific solution as it makes use of alias_method_chain.


module ConditionallyOverrideDelegation
def self.included(base)
base.extend(Macro)
end
module Macro

# class Foo
# extend Forwardable
# def_delegator :bar, :name
# conditionally_override_delegation :name
# end
# class Bar
# def name
# 'Bar'
# end
# end
#
# @foo = Foo.new
# @foo.bar.name == @foo.name
# => true
#
# @foo.name_with_delegation_override = @foo.bar.name * 2
# @foo.bar.name == @foo.name
# => false
#
# Take a look at the tests to see how this truly behaves
#
# Really you probably only want to use this method in test
#
def conditionally_override_delegation(*targets)
targets.flatten.each do |target|
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
src = <<-EOV
def #{aliased_target}_with_delegation_override#{punctuation}(*args)
if @#{aliased_target}_with_delegation_override.nil?
#{aliased_target}_without_delegation_override#{punctuation}(*args)
else
@#{aliased_target}_with_delegation_override
end
end
EOV
module_eval src, __FILE__, __LINE__
alias_method_chain target, :delegation_override
attr_writer "#{aliased_target}_with_delegation_override"
end
end

end
end




And the obligatory tests


require File.dirname(__FILE__) + '/../test_helper'
require 'conditionally_override_delegation'
class ConditionallyOverrideDelegationTest < Test::Unit::TestCase
class AbstractUnit
extend Forwardable
include ConditionallyOverrideDelegation

def_delegators :related_object, :to_s, :string=, :string, :string?

conditionally_override_delegation :to_s, :string

def related_object
@related_object ||= RelatedObject.new
end
end

class RelatedObject
def to_s
'Hello World'
end
attr_accessor :string
def string?
!!string
end
end

def setup
@unit = AbstractUnit.new
end

def test_delegation_is_working
assert_equal @unit.related_object.to_s, @unit.to_s
end

def test_overridding_delegation_is_working
@unit.to_s_with_delegation_override = 'Turkey Sandwich'
assert_equal 'Turkey Sandwich', @unit.to_s
end

def test_overridding_delegation_is_working_when_override_is_false
@unit.to_s_with_delegation_override = false
assert_equal false, @unit.to_s
end

def test_overridding_delegation_is_working_when_override_is_nil
@unit.to_s_with_delegation_override = nil
assert_equal @unit.related_object.to_s, @unit.to_s
end

def test_overridding_delegation_is_working_with_punctuation
@unit.related_object.string = 'Troy Mclure'
assert_equal 'Troy Mclure', @unit.string

@unit.string_with_delegation_override = 'Pancake'
assert_equal 'Pancake', @unit.string
end
end

No comments: