Saturday, August 22, 2009

Unpublished Cucumbers

‹prev | My Chain | next›

Before this new CouchDB / Sinatra version of EEE Cooks is ready to replace the legacy rails site, there are a few more features that need to be completed:
  • draft meals and recipes
  • alternate preparations for recipes
  • obsolete recipes (replaced by newer recipes)
  • mini-calendar for the homepage
After first ensuring that I have Cucumber feature descriptions for each of these things (commit, commit), I get started on the draft document feature.

I write the feature from the perspective of the cookbook. I considered writing it from the perspective of a web user, but why would a user care about draft vs. published recipes? Someone reading a cookbook does not care how the recipes got there, just that the recipes that they see are of a certain quality. I am not building an author interface—I will likely do that in a separate application another day. So, it seemed proper to anthropomorphize the cookbook, making it care about the quality control:
Feature: Draft vs. Published Meals and Recipes

As a cookbook dedicated to quality
So that I can present only the best meals and recipes
I want to hide drafts
The first scenario deals with how the cookbook hides draft recipes from the user:
    Scenario: Navigating between recipes
Given "Recipe #1", published on 2009-08-01
And "Recipe #2", drafted on 2009-08-05
And "Recipe #3", published on 2009-08-10
And "Recipe #4", drafted on 2009-08-20
When I show "Recipe #1"
Then there should be no link to "Recipe #2"
When I am asked for the next recipe
Then "Recipe #3" should be shown
And there should be no next link
And there should be a link to "Recipe #1"
I can get those three Given steps defined in one place:
Given /^"([^\"]*)", (\w+) on ([-\d]+)$/ do |title, status, date_str|
date = Date.parse(date_str)
recipe_permalink = date.to_s + "-" + title.downcase.gsub(/[#\W]+/, '-')

recipe = {
:title => title,
:date => date,
:summary => "#{title} summary",
:instructions => "#{title} instructions",
:published => status == 'published',
:type => "Recipe"
}

RestClient.put "#{@@db}/#{recipe_permalink}",
recipe.to_json,
:content_type => 'application/json'
end
Each Given step has a title, a status, and a date in exactly the same place in the Given string. Cucumber RegExp support comes in quite handy here. The recipe creation is very similar to other recipe step definitions that I have done. The only reason that I needed a new step definition at all was the :published attribute. That survived the import from the legacy Rails application, but is doing nothing in the application just yet.

The first step towards using the :published attribute is to probe what happens when the cookbook displays the first recipe:
When /^I show "Recipe #1"$/ do
visit("/recipes/#{@recipe_1_permalink}")
end
I opt not to use the template generated by Cucumber for this step, which would have read:
When /^I show "([^\"]*)"$/ do |arg1|
pending
end
The title alone (the stuff that would have been between the quotes) is not sufficient for determining the URL of the recipe. I could alter the feature text to read:
When I show "Recipe #1" from 2009-08-01
But then I would be duplicating the date in both the Given and When steps only to support the Cucumber steps. I loathe the idea of sacrificing the readability of the feature text just to support step definitions.

With the When step out of the way, I define my first Then step—that there should be no link to the draft recipe—as:
Then /^there should be no link to "([^\"]*)"$/ do |title|
response.should_not have_selector("a", :content => title)
end
This step fails:
cstrom@jaynestown:~/repos/eee-code$ cucumber features/draft_recipes.feature:7
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Draft vs. Published Meals and Recipes

As a cookbook dedicated to quality
So that I can present only the best meals and recipes
I want to hide drafts

Scenario: Navigating between recipes # features/draft_recipes.feature:7
Given "Recipe #1", published on 2009-08-01 # features/step_definitions/draft.rb:1
And "Recipe #2", drafted on 2009-08-05 # features/step_definitions/draft.rb:1
And "Recipe #3", published on 2009-08-10 # features/step_definitions/draft.rb:1
And "Recipe #4", drafted on 2009-08-20 # features/step_definitions/draft.rb:1
When I show "Recipe #1" # features/step_definitions/draft.rb:22
Then there should be no link to "Recipe #2" # features/step_definitions/draft.rb:26
expected following output to omit a <a>Recipe #2</a>:
...
<div class="navigation">


|
<a href="/recipes/2009-08-05-recipe-2">Recipe #2 (August 5, 2009)</a>

</div>
...
(Spec::Expectations::ExpectationNotMetError)
features/draft_recipes.feature:13:in `Then there should be no link to "Recipe #2"'
When I am asked for the next recipe # features/draft_recipes.feature:14
Then "Recipe #3" should be shown # features/draft_recipes.feature:15
And there should be no next link # features/draft_recipes.feature:16
And there should be a link to "Recipe #1" # features/draft_recipes.feature:17

Failing Scenarios:
cucumber features/draft_recipes.feature:7 # Scenario: Navigating between recipes

1 scenario (1 failed)
10 steps (1 failed, 4 undefined, 5 passed)
0m0.583s
This signals to me that I need to work my way into the code—draft recipes are still being shown. But...

The links between recipes (like intra-meal links) are being built by a helper method that, in turn, uses the results of a CouchDB view. The by-date map function that I am currently using:
function (doc) {
if (doc['type'] == 'Recipe') {
emit(doc['date'], {'id':doc['_id'],'title':doc['title'],'date':doc['date']});
}
}
All I need to do is add to the conditional in there:
function (doc) {
if (doc['type'] == 'Recipe' && doc['published'] == true) {
emit(doc['date'], {'id':doc['_id'],'title':doc['title'],'date':doc['date']});
}
}
I am using my couch_docs gem in a Before block to assemble and load those design docs before each Cucumber run. So all I have to is re-run the scenario:



Cool, that should be all of the actual work required for this scenario. The remaining step definitions are all one liners:
When /^I am asked for the next recipe$/ do
click_link "Recipe #"
end

Then /^"([^\"]*)" should be shown$/ do |title|
response.should have_selector("h1", :content => title)
end

Then /^there should be no next link$/ do
response.should_not have_selector("a", :content => "Recipe #4")
end

Then /^there should be a link to "([^\"]*)"$/ do |title|
response.should have_selector("a", :content => title)
end
With that, the scenario is complete:



A previous scenario does end up failing because of the new published restriction. After addressing that (by publishing the recipes in the Given step), I have 36 completed scenarios:
cstrom@jaynestown:~/repos/eee-code$ cucumber features -i 
...
36 scenarios (7 undefined, 29 passed)
308 steps (17 skipped, 36 undefined, 255 passed)
0m35.558s
(commit)

Tomorrow, I hope to complete the published vs. draft feature. It ought to be straight forward to get this working for both recipe searching and meal navigation.

No comments:

Post a Comment