Monday, May 25, 2009

Meals by Month Goes Green (with a little helper from Cucumber)

‹prev | My Chain | next›

I left off yesterday with a failing Cucumber step, making it easy to know where to start today:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features -s "Browsing a meal in a given month"
Feature: Browse Meals

So that I can find meals made on special occasions
As a person interested in finding meals
I want to browse meals by date

Scenario: Browsing a meal in a given month
Given a "Even Fried, They Won't Eat It" meal enjoyed in May of 2009
And a "Salad. Mmmm." meal enjoyed in April of 2009
When I view the list of meals prepared in May of 2009
Then I should see the "Even Fried, They Won't Eat It" meal among the meals of this month
And I should not see the "Salad. Mmmm." meal among the meals of this month
And I should not see a link to June of 2009
When I follow the link to the list of meals in April of 2009
Then I should not see the "Even Fried, They Won't Eat It" meal among the meals of this month
And I should see the "Salad. Mmmm." meal among the meals of this month
expected following output to contain a <h2>Salad. Mmmm.</h2> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<h1>Meals from 2009</h1>
<div class="navigation">

|

</div>
<ul>
<li>
<a href="/meals/id-2009-05-01">Even Fried, They Won't Eat It</a>
</li>
</ul>
</body></html>
(Spec::Expectations::ExpectationNotMetError)
features/browse_meals.feature:28:in `And I should see the "Salad. Mmmm." meal among the meals of this month'
And I should not see a link to February of 2009
And I should see a link to May of 2009

1 scenario
1 failed step
2 undefined steps
8 passed steps
I barely even glanced at the failure yesterday, but today I notice that the H1 title is describing "Meals from 2009" rather than "Meals from April, 2009". Somehow the link being followed to "to the list of meals in April of 2009" is taking Cucumber to the list of meals in 2009 instead.

Before in-depth investigation of the helper responsible for those links, I think now is a good time to align the language of the helper with its usage. Specifically, it should be less year-oriented and more date fragment-oriented:
    def link_to_year_in_set(current, couch_view, options={})
compare_years = options[:previous] ?
Proc.new { |year, current_year| year < current_year} :
Proc.new { |year, current_year| year > current_year}

next_result = couch_view.
send(options[:previous] ? :reverse : :map).
detect{|result| compare_years[result['key'].to_i, current.to_i]}

if next_result
%Q|<a href="/meals/#{next_result['key']}">#{next_result['key']}</a>|
else
""
end
end
The more date fragment oriented rewrite:
    def link_to_adjacent_view_date(current, couch_view, options={})
compare = options[:previous] ?
Proc.new { |date_fragment, current| date_fragment < current} :
Proc.new { |date_fragment, current| date_fragment > current}

next_result = couch_view.
send(options[:previous] ? :reverse : :map).
detect{|result| compare[result['key'], current.to_s]}

if next_result
%Q|<a href="/meals/#{next_result['key']}">#{next_result['key']}</a>|
else
""
end
end
After updating various specs and consuming Haml templates to use the renamed function, I take a closer look at the link output. The CouchDB by_month map-reduce views that are of the form (after being parsed into Ruby):
{"value"=>3, "key"=>"2009-04"}
The value indicates the total number of meals in the month indicated by the key (in the above example, there were 3 meals from April of 2009).

Looking back to the output of the helper, I would end up with the following for the "2009-04" example:
<a href="/meals/2009-04">2009-04</a>
The problem is that the Sinatra app does not recognize "/meals/2009-04" as a by-month URL because the dash does match the RegExp:
get %r{/meals/(\d+)/(\d+)} do |year, month|
...
end
That URL does match the by-year action (which completely ignores the month portion of the URL):
get %r{/meals/(\d+)} do |year|
...
end
Yet again, Cucumber has caught a composition error on my part. The individual components all work according to specification—one can easily make the case that the URL "/meal/2009-04" is a valid by-month URL—but the assembly of those pieces into a whole missed a vital piece of information.

I could address this oversight by making the URL accept dashes or by making the helper replace dashes with slashes. I opt for the latter—I simply prefer the simplicity of an all slash URL.

The example that I use to describe this behavior, including a by-month context, is:
  context "couchdb view by_month" do
before(:each) do
@count_by_month = [{"key" => "2009-04", "value" => 3},
{"key" => "2009-05", "value" => 3}]
end
it "should link to the next month after the current one" do
link_to_adjacent_view_date("2009-04", @count_by_month).
should have_selector("a",
:href => "/meals/2009/05")
end
end
That fails, as expected, because the href still has a dash in it:
cstrom@jaynestown:~/repos/eee-code$ spec spec/eee_helpers_spec.rb 
.............................F

1)
'link_to_adjacent_view_date couchdb view by_year should link to the next month after the current one' FAILED
expected following output to contain a <a href='/meals/2009/05'/> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><a href="/meals/2009-05">2009-05</a></body></html>
./spec/eee_helpers_spec.rb:228:

Finished in 0.025192 seconds

30 examples, 1 failure
To make it pass, I add a computation of the next_uri to the helper:
    def link_to_adjacent_view_date(current, couch_view, options={})
compare = options[:previous] ?
Proc.new { |date_fragment, current| date_fragment < current} :
Proc.new { |date_fragment, current| date_fragment > current}

next_result = couch_view.
send(options[:previous] ? :reverse : :map).
detect{|result| compare[result['key'], current.to_s]}

if next_result
next_uri = next_result['key'].gsub(/-/, '/')
%Q|<a href="/meals/#{next_uri}">#{next_result['key']}</a>|

else
""
end
end
Not only does that make that example pass, but, working my way back out to the Cucumber scenario, I find that all currently defined step are passing as well!



Even better, those last two steps are trivial to implement:
Then /^I should not see a link to February of 2009$/ do
response.should_not have_selector("a", :content => "2009-02")
end

Then /^I should see a link to May of 2009$/ do
response.should have_selector("a", :content => "2009-05")
end
And now the entire scenario is passing:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features \
-s "Browsing a meal in a given month"
Feature: Browse Meals

So that I can find meals made on special occasions
As a person interested in finding meals
I want to browse meals by date

Scenario: Browsing a meal in a given month
Given a "Even Fried, They Won't Eat It" meal enjoyed in May of 2009
And a "Salad. Mmmm." meal enjoyed in April of 2009
When I view the list of meals prepared in May of 2009
Then I should see the "Even Fried, They Won't Eat It" meal among the meals of this month
And I should not see the "Salad. Mmmm." meal among the meals of this month
And I should not see a link to June of 2009
When I follow the link to the list of meals in April of 2009
Then I should not see the "Even Fried, They Won't Eat It" meal among the meals of this month
And I should see the "Salad. Mmmm." meal among the meals of this month
And I should not see a link to February of 2009
And I should see a link to May of 2009

1 scenario
11 passed steps
Yay!
(commit)

My red-green-refactor development cycle has gotten me to green. There is a definite opportunity to refactor the should / should not see a link to steps. I will tackle that next before working my way onto the next scenario.

No comments:

Post a Comment