Delegation is easy to implement in Ruby, but if you're working test-first and trying to stick to strict unit tests, it can be a challenge to specify delegation succinctly. (Some feel that delegation is so simple that it's a waste to have tests for it. This depends on how you use your tests. This isn't an article about that. If your team isn't interested in unit testing delegation, don't do it. If you find it valuable, read on.)
I recently had a fairly long discussion with Jay during a developer lunch at our current project about unit testing delegation. He and Z had implemented unit tests that mocked the
def_delegators method (of
Forwardable) in the class under test to reduce the size of some very wordy tests.
The tests they discovered looked something like the below, mocking the method used to access the delegate and returning a mock delegate that expected the delegate method to be invoked.
test 'delegates title to content_provider' do
o = ClassBeingTested.new
o.expects(:content_provider).returns(mock(:title => :some_title))
assert_same :some_title, o.title
end
test 'delegates sub_title to content_provider' do
o = ClassBeingTested.new
o.expects(:content_provider).returns(mock(:sub_title => :some_title))
assert_same :some_title, o.title
end
test 'delegates help_text to content_provider' do
o = ClassBeingTested.new
o.expects(:content_provider).returns(mock(:sub_title => :some_title))
assert_same :some_title, o.title
end
Or maybe it was more like this. I don't remember which.
test 'delegates to content_provider for title, sub_title, and help_text' do
o = ClassBeingTested.new
o.expects(:content_provider).at_least_once.returns(
mock(
:title => :some_title,
:sub_title => :some_sub_title,
:help_text => :some_help_text))
assert_same :some_title, o.title
assert_same :some_sub_title, o.sub_title
assert_same :some_help_text, o.help_text
end
In any case, they needed to add some new delegating behavior to the class, and they thought what had been written was awfully wordy. Maybe you think five lines to test three delegating methods is pretty good, but consider that the code under test is something like:
class ClassUnderTest
extend Forwardable
def_delegators :content_provider, :title, :sub_title, :help_text
# ... plus stuff we're not talking about here, including the method
# 'content_provider' that gets the delegate.
end
The delegation of all three methods is just one line of code! (That is, if you don't count the line where the class extends
Forwardable.) Since implementing delegation is that easy, why should we have to write so much code to test the behavior?
They thought about it and tried another strategy: since the delegation plumbing is provided by
Forwardable, they would test that its class methods were called correctly by mocking them out. Their new test did something like:
test 'delegates price and has_rebate? to model' do
ClassBeingTested.expects(:def_delegators).with(:model, :price, :has_rebate?)
load 'class_being_tested'
end
The method
load is like
require, but without caching its results and turning no-op when the file has already been loaded. It forces the class definition to run again, calling
def_delegators again.
A clever solution, but it felt wrong to me.
Mock-based testing requires us to dictate to some extent what our implementation will look like. This is an evil we live with when it also provides a fairly readable description of a class's responsibilities. But here, we seemed to have lost site of the responsibility. The responsibility we want to show is that the object delegates. How it accomplishes that delegation is a detail we'd rather leave to the class. The more closely a test is tied to implementation, the more likely it is that we'll need to change the test when we change the class, even if the class's responsibilities are not being changed.
What made the test great was its brevity, but looking back at the original tests, I saw that we could have something just as brief, more readable, and which was not tied to the specific implementation. I wanted a test that looked something like this:
test 'delegates price and has_rebate? to model' do
assert(ClassUnderTest.new).delegates_to(:model).for_methods(:price, :has_rebate?)
end
but that actually exercised the delegating behavior similar to the long-hand tests we started with, so that whether delegation was declared with def_delegator, def_delegators, or implemented manually, the tests would be happy.
A
fluent interface for testing simple delegation is achievable because the responsibility we're testing is so straightforward:
"When I send price or has_rebate? to the object, it should send the same messages to the object it gets from its model method and return what that returns, without any knowledge of the thing returned."
The drawback to a fluent interface is that it can be difficult to debug failures, particularly those due to misuse of the interface itself. It's easy to imagine a developer writing the following and expecting it to work.
assert(ClassUnderTest.new).delegates_to(:model)
Although that reads nicely, it doesn't actually say enough to be able to test anything. We have to be able to implement the interface in such a way that this sort of misuse won't result in tests silently doing nothing.
This seemed like a solvable problem, so I set out to implement it. The result is now available as
Handoff (via
gem install handoff).
As the interface worked out, assertions look like this:
assert_handoff.from(ClassUnderTest.new).to(:content_provider).for(:title, :sub_title)
It supports method names that differ between the delegating object and the delegate, as well as specification of arguments that should be propagated, for example.
assert_handoff.from(ClassUnderTest.new).to(:foo).for(:bar => :foo_bar, :baz => :foo_baz).with('arg1', :arg2, 3)Give it a try.
Labels: fluent interfaces, tdd