Class Ad < ActiveRecord::BaseOften, I end up writing tests for something even as simple as delegation, since I try to TDD all the code I write.
extend Forwardable
def_delegator :lister, name, lister_name
def lister
...
end
end
class AdTest < Test::Unit::TestCaseAfter 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.
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
class AdTest < Test::Unit::TestCaseThe above delegation testing DSL allows me to test multiple validations without the need for many (very similar) tests.
Ad.delegates do
lister_name.to(:lister).via(:name)
lister_address.to(:lister).via(:address)
...
end
end
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 DelegateAssertionThe resulting tests make it hard to justify leaving of tests, even if you are 'only doing simple delegation'.
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
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.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.