Tuesday, June 2, 2009

Breadcrumbs, Driven by Example

‹prev | My Chain | next›

Picking up from yesterday, I need to ensure that breadcrumbs are displayed on pages starting from the "deepest", the recipe, all the way on up to the list of meals in a given year. I could manually code that on each page, but there would be redundancy (link to a year would be on several pages), so I'll do this as a helper instead.

The complete RSpec description of the breadcrumbs helper:
describe "breadcrumbs" do
context "for a year (list of meals in a year)" do
it "should link home"
it "should show the year"
end
context "for a month (list of meals in a month)" do
it "should link home"
it "should link to the year"
it "should show the month"
end
context "for a day (show a single meal)" do
it "should link home"
it "should link to the year"
it "should link to the month"
it "should show the day"
end
context "for a recipe" do
it "should link home"
it "should link to the year"
it "should link to the month"
it "should link to the day"
end
end
I think I would like the helper method to accept two arguments: a date and symbol describing the current context. For example:
describe "breadcrumbs" do
context "for a year (list of meals in a year)" do
it "should link home" do
breadcrumbs(Date.new(2009, 6, 2), :year).
should have_selector("a", :href => "/")
end
end
end
The first time I run the spec, get a bunch of pending specs (the it statements for the other contexts, which lack a block example) and a failure due to the lack of an actual breadcrumbs helper method:
1)
NoMethodError in 'breadcrumbs for a year (list of meals in a year) should link home'
undefined method `breadcrumbs' for #
./spec/eee_helpers_spec.rb:261:
I define the helper:
    def breadcrumbs
end
Which changes the message:
1)
ArgumentError in 'breadcrumbs for a year (list of meals in a year) should link home'
wrong number of arguments (2 for 0)
./spec/eee_helpers_spec.rb:261:in `breadcrumbs'
./spec/eee_helpers_spec.rb:261:
I change the arity of the method to accept two arguments:
    def breadcrumbs(date, context=nil)
end
Which changes the message again:
1)
'breadcrumbs for a year (list of meals in a year) should link home' FAILED
expected following output to contain a <a href='/'/> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">

./spec/eee_helpers_spec.rb:261:
Finally, I can add some code to the body of the helper method:
    def breadcrumbs(date, context=nil)
%Q|<a href="/">home</a>|
end
Which makes the spec pass.

This change-the-message or make-it-pass recursion can seem tedious at first glance, but it is well worth the effort (and really, it's not that much effort). Everything flows naturally from the Cucumber scenario, down to the RSpec example, then through the change-the-message recursion. I progress steadily toward a solution. Any mistakes along the way are easy ones to spot and correct. Once I have exited the change-the-message recursion by making it pass, I know where to pick up next (the next spec).

In this case:
    it "should show the year" do
breadcrumbs(Date.new(2009, 6, 2), :year).
should have_selector("span", :content => "2009")
end
There are no messages to change this time:
1)
'breadcrumbs for a year (list of meals in a year) should show the year' FAILED
expected following output to contain a <span>2009</span> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><a href="/">home</a></body></html>
./spec/eee_helpers_spec.rb:265:
I only need to get it to pass. The simplest possible solution:
    def breadcrumbs(date, context=nil)
%Q|<a href="/">home</a> > <span>#{date.year}</span>|
end
Clearly, that will not be a long-term solution, but I will resist the temptation to generalize at this point. I will let subsequent specs drive the solution.

The completion of the first two breadcrumbs examples also marks the completion of the "for a year" context. Next up, I move down the breadcrumb trail to the "for a month" context. The first example in there is suspiciously similar to the first example in the "for a year" context:
  context "for a month (list of meals in a month)" do
it "should link home"
it "should link to the year"
it "should show the month"
end
That is not just similar, it is identical—including the meaning. Many developers will see this as an opportunity to adhere to DRY principals by creating a helper method or via some other indirection. I hate indirection in tests. Tests should favor readability over all else. Hunting for a helper method or trying to grok their meaning once they are found is not conducive to grok-ability.

Ultimately, I have no trouble at all violating DRY in tests:
    it "should link home" do
breadcrumbs(Date.new(2009, 6, 2), :month).
should have_selector("a", :href => "/")
end
This passes without change.

After my next two "for a month" context examples:
  context "for a month (list of meals in a month)" do
it "should link to the year" do
breadcrumbs(Date.new(2009, 6, 2), :month).
should have_selector("a", :href => "/meals/2009")
end
it "should show the month" do
breadcrumbs(Date.new(2009, 6, 2), :month).
should have_selector("span", :content => "June")
end
end
I end with this definition for breadcrumbs:
    def breadcrumbs(date, context=nil)
crumbs = [ %Q|<a href="/">home</a>| ]

if context == :year
crumbs << %Q|<span>#{date.year}</span>|
else
crumbs << %Q|<a href="/meals/#{date.year}">#{date.year}</span>|
end

if context == :month
crumbs << %Q|<span>#{date.strftime("%B")}</span>|
end

crumbs.join(" &gt; ")
end
After working through the :day and nil (recipe) cases, I end up with this definition for the breadcrumbs helper:
    def breadcrumbs(date, context=nil)
crumbs = [ %Q|<a href="/">home</a>| ]

if context == :year
crumbs << %Q|<span>#{date.year}</span>|
else
crumbs << %Q|<a href="/meals/#{date.year}">#{date.year}</span>|
end

if context == :month
crumbs << %Q|<span>#{date.strftime("%B")}</span>|
elsif context == :day || context == nil
crumbs << %Q|<a href="#{date.strftime("/meals/%Y/%m")}">#{date.strftime("%B")}</a>|
end

if context == :day
crumbs << %Q|<span>#{date.day}</span>|
elsif context == nil
crumbs << %Q|<a href="#{date.strftime("/meals/%Y/%m/%d")}">#{date.day}</a>|
end

crumbs.join(" &gt; ")
end
Not too bad. It's a little long for my taste, but I know that there is absolutely no superfluous code. If you know a feature is done when there is nothing left to take away, I can confidently consider the breadcrumbs helper done.

After replacing my previous in-template breadcrumb implementation with my new helper and ensuring that I have breadcrumbs in all appropriate templates, I am ready to work my way back out to my Cucumber scenario to see if I am finally done with viewing meals:



Yay!
(commit)

Up next, I think I need to play with using Rack a little bit.

No comments:

Post a Comment