This post originated from an RSS feed registered with Ruby Buzz
by Jay Fields.
Original Post: Ruby: Unit testing delegations
Feed Title: Jay Fields Thoughts
Feed URL: http://blog.jayfields.com/rss.xml
Feed Description: Thoughts on Software Development
When working with Rails I tend to use a lot of delegations. Since I'm a big fan of the Law of Demeter I often find myself delegating an_ad_instance.lister_name to the name method of the lister attribute.
Class Ad < ActiveRecord::Base extend Forwardable def_delegator :lister, name, lister_name def lister ... end end
Often, I end up writing tests for something even as simple as delegation, since I try to TDD all the code I write.
class AdTest < Test::Unit::TestCase def test_lister_name_is_delegated_to_listers_name ad = Ad.new(:lister => Lister.new(:name => 'lister_name')) assert_equal 'lister_name', ad.lister_name end end
After a few of these tests, the duplication begins to tell me that I can do something simpler with some metaprogramming. So, I stole the idea from Validatable and created some delegation custom assertions.
class AdTest < Test::Unit::TestCase Ad.delegates do lister_name.to(:lister).via(:name) lister_address.to(:lister).via(:address) ... end end
The above delegation testing DSL allows me to test multiple validations without the need for many (very similar) tests.
The implementation to get this working is fairly straightforward: I create a subclass of the class under test, add a constructor to the subclass that takes the objec tthat's going to be delegated to, and set the value of the attribute on the delegate object. I also add an attr_accessor to the subclass, since all I care about is the delegation (for this test). It doesn't matter to me that I'm changing the implementation of the lister method because I'm testing the delegation, not the lister method.
class DelegateAssertion attr_accessor :desired_method, :delegate_object, :original_method
def initialize(desired_method) self.desired_method = desired_method end
def to(delegate_object) self.delegate_object = delegate_object self end
def via(original_method) self.original_method = original_method end end
class DelegateCollector def self.gather(block) collector = new collector.instance_eval(&block) collector.delegate_assertions end
attr_accessor :delegate_assertions
def delegate_assertions @delegate_assertions ||= [] end
def method_missing(sym, *args) assertion = DelegateAssertion.new(sym) delegate_assertions << assertion assertion end end
class Class def delegates(&block) test_class = eval "self", block.binding assertions = DelegateCollector.gather(block) assertions.each do |assertion| klass = Class.new(self) klass.class_eval do attr_accessor assertion.delegate_object define_method :initialize do |delegate_object| self.send :"#{assertion.delegate_object}=", delegate_object end end test_class.class_eval do define_method "test_#{assertion.desired_method}_is_delegated_to_#{assertion.delegate_object}_via_#{assertion.original_method}" do klass_instance = klass.new(stub(assertion.original_method => :original_value)) begin assert_equal :original_value, klass_instance.send(assertion.desired_method) rescue Exception => ex add_failure "Delegating #{assertion.desired_method } to #{assertion.delegate_object} via #{assertion.original_method} doesn't work" end end end end end end
The resulting tests make it hard to justify leaving of tests, even if you are 'only doing simple delegation'.
note: In the example I use an ActiveRecord object, but since I'm adding this behavior to Class any class can take advantage of these custom validations.