Friday, October 2, 2009

Inside Out Ingredients

‹prev | My Chain | next›

I should be done with the ingredient index page. I have a CouchDB view set up to pull this information back from the server. I have the Sinatra resource pulling from said CouchDB view. I even have the Haml view displaying the data.

I should be done, but I need verify that the pieces fit together, which is what Cucumber is for. The scenario from which all this started now stands at:
jaynestown% cucumber features/ingredient_index.feature:7 -s                     
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Ingredient index for recipes

As a user curious about ingredients or recipes
I want to see a list of ingredients
So that I can see a sample of recipes in the cookbook using a particular ingredient

Scenario: A couple of recipes sharing an ingredient
Given a "Cookie" recipe with "butter" and "chocolate chips"
And a "Pancake" recipe with "flour" and "chocolate chips"
When I visit the ingredients page
Then I should see the "chocolate chips" ingredient
And "chocolate chips" recipes should include "Cookie" and "Pancake"
And I should see the "flour" ingredient
And "flour" recipes should include only "Pancake"

1 scenario (1 undefined)
7 steps (4 undefined, 3 passed)
0m2.190s

You can implement step definitions for undefined steps with these snippets:

Then /^I should see the "([^\"]*)" ingredient$/ do |arg1|
pending
end

Then /^"([^\"]*)" recipes should include "([^\"]*)" and "([^\"]*)"$/ do |arg1, arg2, arg3|
pending
end

Then /^"([^\"]*)" recipes should include only "([^\"]*)"$/ do |arg1, arg2|
pending
end
I define the first step as:
Then /^I should see the "([^\"]*)" ingredient$/ do |ingredient|
response.should have_selector(".ingredient",
:content => ingredient)
end
Running the scenario, I find:
jaynestown% cucumber features/ingredient_index.feature:7 -s
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Ingredient index for recipes

As a user curious about ingredients or recipes
I want to see a list of ingredients
So that I can see a sample of recipes in the cookbook using a particular ingredient

Scenario: A couple of recipes sharing an ingredient
Given a "Cookie" recipe with "butter" and "chocolate chips"
And a "Pancake" recipe with "flour" and "chocolate chips"
When I visit the ingredients page
Then I should see the "chocolate chips" ingredient
expected following output to contain a <.ingredient>chocolate chips</.ingredient> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head>
<title>EEE Cooks: Ingredient Index</title>
<link href="/stylesheets/style.css" rel="stylesheet" type="text/css">
</head>
<html><body>
<div id="header">
<div id="eee-header-logo">
<a href="/">
<img alt="Home" src="/images/eee_corner.png"></a>
</div>
</div>
<h1>
Ingredient Index
</h1>
<table><tr>
<td class="col1">
<p>
<span class="ingredient">
value
</span>
<span class="recipes">
<a href="/recipes/"></a>
</span>
</p>
...
Ew. An ingredient of "value"? A quick investigation identifies a discrepancy between that the view expects and what CouchDB returns (missing the "keys" attribute in the specification).

I could have just as easily viewed the page in a browser to see this error. The benefit of using Cucumber to find this error, of course, is that I never have to manually find this bug in a browser again. I have a high degree of confidence that I have an integration test that is valuable (i.e. it actually found a bug).

After fixing that, I define the next missing step as:
Then /^"([^\"]*)" recipes should include "([^\"]*)" and "([^\"]*)"$/ do |ingredient, arg2, arg3|
response.should have_selector(".recipes") do |span|
span.should have_selector("a", :content => arg2)
span.should have_selector("a", :content => arg3)
end
end
The response ought to have an element with a class="recipes". That element should have child <a> elements, which should be linking the recipe titles from the feature ("chocolate chips" recipes should include "Cookie" and "Pancake").

Last up, I need a definition that fits:
Then "flour" recipes should include only "Pancake"
For this, I need to break out the XPath. I am looking to verify that a <p> tag contains a span with the "flour" ingredient and that also contains another span that has the recipe title somewhere in it. The step definition that verifies this:
Then /^"([^\"]*)" recipes should include only "([^\"]*)"$/ do |ingredient, recipe|
response.should have_xpath("//p[contains(span, '#{ingredient}')]/span[contains(., '#{recipe}')]")
end
For good measure, I would like to verify that there is only one <a> tag associated with that ingredient, so I add a second XPath expression:
Then /^"([^\"]*)" recipes should include only "([^\"]*)"$/ do |ingredient, recipe|
response.should have_xpath("//p[contains(span, '#{ingredient}')]/span[contains(., '#{recipe}')]")
response.should have_xpath("//p[contains(span, '#{ingredient}')]/span[count(a)=1]")
end
Just like that, I have a passing scenario:
jaynestown% cucumber features/ingredient_index.feature:7 -s
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Ingredient index for recipes

As a user curious about ingredients or recipes
I want to see a list of ingredients
So that I can see a sample of recipes in the cookbook using a particular ingredient

Scenario: A couple of recipes sharing an ingredient
Given a "Cookie" recipe with "butter" and "chocolate chips"
And a "Pancake" recipe with "flour" and "chocolate chips"
When I visit the ingredients page
Then I should see the "chocolate chips" ingredient
And "chocolate chips" recipes should include "Cookie" and "Pancake"
And I should see the "flour" ingredient
And "flour" recipes should include only "Pancake"

1 scenario (1 passed)
7 steps (7 passed)
0m0.665s
I have one more scenario describing the case in which "common" ingredients are excluded from the index. No one really wants to scan through an index and be confronted with 200+ recipes with salt in them. I will pick up with that scenario tomorrow.

2 comments:

  1. This one bothers me:

    And "chocolate chips" recipes should include "Cookie" and "Pancake"

    How about

    Then "chocolate chips" recipe should include the following:
    | Cookie |
    | Pancake |


    Cucumber tables are neat, and then you don't limit yourself to just two ingredients

    ReplyDelete
  2. Hmmm... I agree that the wording is a bit awkward. This is probably better:

    And the list of recipes with "chocolate chips" in them should include the "Cookie" and "Pancake" recipes

    I still prefer having it all on one line, though. I'd rather do a little work in the step definition than force the human reader to have to ponder the implications of such a table.

    If I needed to support more than two ingredients, I would slurp everything between "should include the" and "recipes" into a single RegExp:


      Then /^the list of recipes with "([^\"]*)" in them should include the (.+) recipes$/ do |ingredient, recipes|
        titles = recipes.split(/\s*and|,\s*/).map{|t| t.gsub(/"/, '')}
        response.should have_selector(".recipes") do |span|
          titles.each do |title|
            span.should have_selector("a", :content => title)
          end
        end
      end

    ReplyDelete