Saturday, May 19, 2007

RailsConf07: Testing Presentation

I don't have a .key (or .ppt) to post from my RailsConf 2007 talk. In fact, the talk was much more of a discussion than a presentation. However, I did go back and tag all the previous entries that contained discussion starting points: http://blog.jayfields.com/search/label/RailsConf2007

Monday, May 14, 2007

Law of Demeter: Canary in a coal mine

I've always been a fan of the Law of Demeter. The Law of Demeter is most often summarized as “Only talk to your immediate friends.”
The fundamental notion is that a given object should assume as little as possible about the structure or properties of anything else (including its subcomponents).
Wikipedia
That's all well and good, but I quite like the Law of Demeter because it's the canary (warning device) in my coal mine (codebase).

I was recently looking over some code with Mike Ward, Shane Harvie, Muness Alrubaie, Martin Fowler, Dan Manges, and David Vollbracht. The code in question had around a hundred lines of def_delegator :object, method_name, new_method_name. No one in the room thought that the 100+ lines of delegation were a good thing, but we all had opinions on how to deal with the ugly code.

Despite the various opinions on solutions, one thing was clear: The Law of Demeter exposed other issues within the codebase. For example, the object in question was taking on far too much responsibility. It had become the DoAnything object of the system. Additionally, methods such as diamond.clarity= were being delegated from ring via the ring.diamond_clarity= method. These methods were required as the attributes of a diamond were changed. However, a possibly better solution would be a method such as ring.replace_diamond that set an entirely new diamond. With the replace_diamond method in place the ring no longer needed to expose the setters of the diamond.

An additional note that came from this conversation: If you are a mockist you will often prefer to follow the Law of Demeter because it makes mocking much easier. However, classicists don't seem to mind as much since they can use train-wreck syntax for state based tests without any pain. Being a mockist or classicist seems to greatly influence a person's opinion on whether or not to follow the Law of Demeter.

Saturday, May 05, 2007

Ruby: Unit testing delegations

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.

Wednesday, May 02, 2007

RailsConf 2007 Presentation

It looks like I'll be attending and speaking at RailsConf 2007.

My talk is going to cover testing lessons learned from various Ruby/Rails projects. More info is available on the official website.

See you in Portland.

Tuesday, May 01, 2007

Ruby: Validatable Tests

In the comments for my most recent entry, Validatable 1.2.2 released, Jon Leighton asks: how do you recommend testing validations?

I've had this discussion before when I wrote about testing ActiveRecord validations. I've not been able to come up with a 'best practice', but I've had some fun trying out different solutions.

My latest attempt at a solution alternative is to create assertions for the framework. For example, if I were to create a Validatable framework, that framework would include ValidatableAssertions.

To create validations for the Validatable framework I wrote my ideal syntax and then figured out how to make it work. The following code was the original version for validating that a class contained a presence of validation.
class FooTest < Test::Unit::TestCase
Foo.must_validate.presence_of :name
end
I originally chose the above syntax because it was expressive enough to convey my intent in the code. I spent a bit of time making this syntax work, but in the end I went with a syntax that I found a bit drier, and easier to implement.
class FooTest < Test::Unit::TestCase
include ValidatableAssertions

Foo.must_validate do
presence_of :name
format_of(:name).with(/^[A-Z]/)
numericality_of(:age).only_integer(true)
end
end
The above code creates a test for each line in the block given to must_validate. If the Foo class does not contain a presence of validation for name, an error with the text "Foo does not contain a Validatable::ValidatesPresenceOf for name" will be raised.

Clearly this solution has limitations. Any validates_true_for validation cannot be tested using this DSL style of testing. Furthermore, any validation that uses an :if argument cannot use this DSL, since those validations require an instance to eval the :if argument against. However, for validations that are not validates_true_for and do not rely on an :if argument, the ValidatableAssertions can replace various existing success and failure validation tests.

If you hate this idea, no worries, you can always test your classes the traditional way: creating an instance and calling the valid? method. This DSL is another tool that may or may not help you out based on the context in which you use it.

The ValidatableAssertions are available in gem version 1.3.0 and greater.