Showing posts with label inside-out. Show all posts
Showing posts with label inside-out. Show all posts

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.

Saturday, June 13, 2009

One Step Closer to a Homepage

‹prev | My Chain | next›

Today, I pick up midway through a step in the Cucumber scenario describing site exploration from the homepage. Before I can move back out to the scenario to check off the step as complete, I need to elaborate some more on the current step: "I should see the 10 most recent meals prominently displayed". The meal titles are already presented on the homepage, but I need to display the summary as well.

The example (in RSpec format) that describes the presentation of the titles on the homepage:
describe "index.haml" do
before(:each) do
assigns[:meals] = [{ "key" => "2009-05-15",
"value" => ['2009-05-15', "Foo"] },
{ "key" => "2009-05-31",
"value" => ["2009-05-31", "Bar"] }]
end

it "should link to the meal titles" do
render("/views/index.haml")
response.should have_selector("h2", :content => "Bar")
end
end
The @meals instance variable is assigned by the Sinatra controller, which retrieves the data structure from a CouchDB view. When I first created the view, I only needed the date and title in the value. Now I need the summary as well. Peeking ahead in the Cucumber scenario, I see that I will need the meal image and menu items as well:
  Scenario: Quickly scanning meals and recipes accessible from the home page
Given 25 yummy meals
And 1 delicious recipe for each meal
And the first 5 recipes are Italian
And the second 10 recipes are Vegetarian
When I view the site's homepage
Then I should see the 10 most recent meals prominently displayed
And the prominently displayed meals should include a thumbnail image
And the prominently displayed meals should include the recipe titles

...
So I need nearly the entire meal. Do I pull the entire meal into the CouchDB view or do I look up each meal individually? I think I will go with the latter. My convention with CouchDB views has been to include only the date. When I implemented the between meal navigation, I added the title. If I switch now to include an entire object, I may end up causing confusion down the line.

So I need to stub out the initial RestClient.get to the CouchDB view to pull back 13 meals (ten for prominent display, 3 more for text links only). The example usage then attempts to verify that the detailed meal information is only request ten times (for prominent display). Thirteen meals make for a long example block:
      it "should pull back full details for the first 10 meals" do
RestClient.
stub!(:get).
and_return('{"rows": [' +
'{"key":"2009-06-10","value":["2009-06-10","Foo"]},' +
'{"key":"2009-06-09","value":["2009-06-09","Foo"]},' +
'{"key":"2009-06-08","value":["2009-06-08","Foo"]},' +
'{"key":"2009-06-07","value":["2009-06-07","Foo"]},' +
'{"key":"2009-06-06","value":["2009-06-06","Foo"]},' +
'{"key":"2009-06-05","value":["2009-06-05","Foo"]},' +
'{"key":"2009-06-04","value":["2009-06-04","Foo"]},' +
'{"key":"2009-06-03","value":["2009-06-03","Foo"]},' +
'{"key":"2009-06-02","value":["2009-06-02","Foo"]},' +
'{"key":"2009-06-01","value":["2009-06-01","Foo"]},' +
'{"key":"2009-05-31","value":["2009-05-31","Foo"]},' +
'{"key":"2009-05-30","value":["2009-05-30","Foo"]},' +
'{"key":"2009-05-29","value":["2009-05-29","Foo"]}' +
']}')

RestClient.
should_receive(:get).
with(/2009-0/).
exactly(10).times.
and_return('{"title":"foo",' +
'"summary":"foo summary",' +
'"menu":[]}')

get "/"

end
end
With the example now failing, I use a range to inject 10 IDs into a meal array for the Haml template:
get '/' do
url = "#{@@db}/_design/meals/_view/by_date?limit=13"
data = RestClient.get url
@meal_view = JSON.parse(data)['rows']

@meals = @meal_view[0...10].inject([]) do |memo, couch_rec|
data = RestClient.get "#{@@db}/#{couch_rec['key']}"
meal = JSON.parse(data)
memo + [meal]
end


haml :index
end
With the Sinatra application behaving as desired, now I need to get the Haml template to present the new data. After updating the before block and the first example to work with the list of full meals, I add two examples describing the summary, checking for presence and wiki-fication:
  it "should include a summary of the meals" do
render("/views/index.haml")
response.should have_selector("p", :content => "Bar summary")
end

it "should wikify the summary" do
assigns[:meals][0]["summary"] = "Foo *bar* baz"
render("/views/index.haml")
response.should have_selector("p strong",
:content => "bar")

end
One line of Haml is needed to make both example pass:
.meals
%h1 Meals
- @meals.each do |meal|
%h2= meal["title"]
%p= wiki(meal["summary"])
With that, I am ready to check this of my Cucumber list by defining the step:
Then /^I should see the 10 most recent meals prominently displayed$/ do
response.should have_selector("h2", :count => 10)
response.should have_selector("h2", :content => "Meal 0")
response.should have_selector("h2", :content => "Meal 9")
response.should_not have_selector("h2", :content => "Meal 10")
end
Running the Cucumber scenario, I find:
cstrom@jaynestown:~/repos/eee-code$ cucumber -ni features \
-s "Quickly scanning meals and recipes accessible from the home page"
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Site

So that I may explore many wonderful recipes and see the meals in which they were served
As someone interested in cooking
I want to be able to easily explore this awesome site

Scenario: Quickly scanning meals and recipes accessible from the home page
Given 25 yummy meals
And 1 delicious recipe for each meal
And the first 5 recipes are Italian
And the second 10 recipes are Vegetarian
When I view the site's homepage
Then I should see the 10 most recent meals prominently displayed
expected following output to contain a <h2>Meal 0</h2> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div class="meals">
<h1>Meals</h1>
<h2>Meal 24</h2>
<p></p>
<p>meal summary</p>
<h2>Meal 23</h2>
<p></p>
<p>meal summary</p>
<h2>Meal 22</h2>
<p></p>
<p>meal summary</p>
<h2>Meal 21</h2>
<p></p>
<p>meal summary</p>
<h2>Meal 20</h2>
<p></p>
<p>meal summary</p>
<h2>Meal 19</h2>
<p></p>
...
Aw man!

After recovering from the initial disappointment, I look closer at the failure to discover that the meals are being returned from the CouchDB view in the opposite order that I wanted. This is a hazard of extensive stubbing in my unit tests. Thankfully, Cucumber caught the problem for me. Adding a descending=true to my GET of the CouchDB view should resolve the problem:
get '/' do
url = "#{@@db}/_design/meals/_view/by_date?limit=13&descending=true"
data = RestClient.get url
@meal_view = JSON.parse(data)['rows']

@meals = @meal_view[0...10].inject([]) do |memo, couch_rec|
data = RestClient.get "#{@@db}/#{couch_rec['key']}"
meal = JSON.parse(data)
memo + [meal]
end

haml :index
end
And running the scenario again, I find that everything is working as desired:



One step closer to a working homepage. Yay!

Friday, June 12, 2009

Finally, a Homepage

‹prev | My Chain | next›

Continuing the homepage-and-beyond Cucumber scenario, next up is adding some categories to the recipes:
  Scenario: Quickly scanning meals and recipes accessible from the home page
Given 25 yummy meals
And 1 delicious recipe for each meal
And the first 5 recipes are Italian
And the second 10 recipes are Vegetarian

When I view the site's homepage
...
In the "1 delicious recipe for each meal" step (implemented last night), the recipe IDs are stored away in an instance variable (@recipe_ids) for later use. I make use of it when adding the first five recipes to the "Italian" category:
Given /^the first 5 recipes are Italian$/ do
@recipe_ids[0...5].each do |recipe_id|
data = RestClient.get "#{@@db}/#{recipe_id}"
recipe = JSON.parse(data)

recipe['tag_names'] = ['italian']

RestClient.put "#{@@db}/#{recipe['_id']}",
recipe.to_json,
:content_type => 'application/json'
end
end
That step operates on each of the first five recipes (0...5 includes 0, 1, 2, 3, and 4 and not 5—two dots instead of three would include 5). For each recipe created in the previous Cucumber step, the recipe's tag names are set to 'italian'. The recipe is then PUT back onto the CouchDB database. As with the meal updates from yesterday, this CouchDB PUT is only possible because the lookup pulls back the most recent revision ID.

I implement a similar step for the next 10 vegetarian recipes, then it is time to visit homepage:
When /^I view the site's homepage$/ do
visit('/')
response.should be_ok
end
This step fails, of course, because I have not created the homepage. So let's move into the code to create it.

I drive the initial development of the homepage with these examples:
    describe "GET /" do
it "should respond OK" do
get "/"
last_response.should be_ok
end

it "should request the most recent 13 meals from CouchDB" do
RestClient.
should_receive(:get).
with(/by_date.+limit=13/).
and_return('{"rows": [] }')

get "/"
end
end
Those two examples ensure that the homepage will respond OK and that it will load 13 meals from CouchDB (10 for pretty display, 3 extra for links only). The code that makes these two examples pass is:
get '/' do
url = "#{@@db}/_design/meals/_view/by_date?limit=13"
data = RestClient.get url
@meals = JSON.parse(data)['rows']

""
end
Next, I remove the empty string from the end of the get "/" block, replacing it with a Haml template call:
  haml :index
So now, it is time to move down into the Haml template. I carry the instance variable @meals down to the Haml template to drive the presentation:
describe "index.haml" do
before(:each) do
assigns[:meals] = [{ "key" => "2009-05-15",
"value" => ['2009-05-15', "Foo"] },
{ "key" => "2009-05-31",
"value" => ["2009-05-31", "Bar"] }]
end

it "should link to the meal titles" do
render("/views/index.haml")
response.should have_selector("h2", :content => "Bar")
end
end
Finally, I implement this in a Haml template:
.meals
%h1 Meals
- @meals.each do |meal|
%h2= meal["value"][1]
That is decent progress for a day. Before quitting, I double-check that the Cucumber step describing accessing the homepage is now passing, which it is:



I could make the first "then" step pass. The h2 headers in the Haml template could be construed as "prominently displayed" meals. I still need to display the meal summary, so I will pick up with that tomorrow, before marking that step as complete.

Friday, May 29, 2009

Simple Meal Attributes

‹prev | My Chain | next›

Next up in the meal details scenario is to actually see some details of the meal. Specifically:
Then I should see the "Focaccia!" title
Cucumber is telling me to use this as starting point:
Then /^I should see the "([^\"]*)" title$/ do |arg1|
pending
end
Little more than that is actually required to verify this step:
Then /^I should see the "([^\"]*)" title$/ do |title|
response.should have_selector("h1", :content => title)
end
That step fails, of course, since the Sinatra action is not pulling back the meal from CouchDB and, even if it was, there is no Haml template to present the CouchDB data. So let's add these...

I implement the body of the show meal action such that it retrieves the meal data from CouchDB:
      it "should request the meal from CouchDB" do
RestClient.
should_receive(:get).
with(/2009-05-13/).
and_return('{"title":"Foo"}')

get "/meals/2009/05/13"
end
With Sinatra pulling the data from CouchDB, it is time to present that data via a Haml view.

For now, I will describe displaying the meal data proper (links to breadcrumbs, other meals and recipes will come later). To display these attributes, I supply each of my examples with:
  before(:each) do
@title = "Meal Title"
@summary = "Meal Summary"
@description = "Meal Description"
assigns[:meal] = @meal = {
'title' => @title,
'summary' => @summary,
'description' => @description
}
end
The specs that describe showing these three meal attributes:
  it "should display the meal's title" do
render("/views/meal.haml")
response.should have_selector("h1", :content => @title)
end

it "should display the meal's summary" do
render("/views/meal.haml")
response.should have_selector("#summary", :content => @summary)
end

it "should display a description of the meal" do
render("/views/meal.haml")
response.should have_selector("#summary + #description",
:content => @description)
end
I still have not decided if I like Haml, but it does make implementing these three examples simple:
%h1
= @meal['title']

#summary
= @meal['summary']

#description
= @meal['description']
Before working my way out, I am also going to note that both the meal summary and description should be wiki-fied. The example for a wiki-fied summary:
  it "should wikify the meal's summary" do
assigns[:meal]['summary'] = "paragraph 1\n\nparagraph 2"
render("/views/meal.haml")
response.should have_selector("#summary p", :content => "paragraph 1")
end
That fails the first time I run it:
1)
'meal.haml should wikify the meal's summary' FAILED
expected following output to contain a <#summary p>paragraph 1</#summary p> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<h1>
Meal Title
</h1>
<div id="summary">
paragraph 1

paragraph 2
</div>
<div id="description">
Meal Description
</div>
</body></html>
./spec/views/meal.haml_spec.rb:28:
Adding a call to my wiki helper makes that pass:
#summary
= wiki @meal['summary']
After following the same path to wiki-fy the meal description, it is time to work my way back out to Cucumber to verify that I have put everything together properly:
cstrom@jaynestown:~/repos/eee-code$ cucumber features -n \
> -s "Browsing a meal on a specific date"
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 on a specific date
Given a "Focaccia!" meal enjoyed on March 3, 2009
And a "Focaccia" recipe from March 3, 2009
When I view the "Focaccia!" meal
Then I should see the "Focaccia!" title
And I should see a link to the "Focaccia" recipe in the menu
expected following output to contain a <a>the "Focaccia" recipe in the menu</a> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<h1>
Focaccia!
</h1>
<div id="summary">
<p>meal summary</p>
</div>
<div id="description">
<p>meal description</p>
</div>
</body></html>
(Spec::Expectations::ExpectationNotMetError)
features/browse_meals.feature:42:in `And I should see a link to the "Focaccia" recipe in the menu'
When I click the "March" link
Then I should see "Focaccia!" in the list of meals
When I click the "Focaccia" link
And I click the "2009" link
Then I should see "Focaccia!" in the list of meals
When I click the "Focaccia!" link
And I click the "Focaccia" link
Then I should see the "Focaccia" recipe

1 scenario
1 failed step
8 undefined steps
4 passed steps
Indeed, I have made it past the Meal should display a title step. I am now onto the next step, where it should display a recipe menu item.

That's a good stopping point for tonight. I like stopping on Red in the Red-Green-Refactor cycle. It makes it easy know where to pick up next!
(commit)

Wednesday, May 20, 2009

Meal Summaries, By Month

‹prev | My Chain | next›

In order to display a list of meals in a month, we need the meal's title, date, summary, list of menu items, and the image. That is pretty much the entire meal, so we might as well pull the entire thing back in the CouchDB view. Thus, the view becomes:
    "by_month": {
"map": "function (doc) {
if (doc['type'] == 'Meal') {
emit(doc['date'].substring(0, 4) + '-' + doc['date'].substring(5, 7), doc);
}
}",
"reduce": "function(keys, values, rereduce) { return values; }"
},
"count_by_month": {
"map": "function (doc) {
if (doc['type'] == 'Meal') {
emit(doc['date'].substring(0, 4) + '-' + doc['date'].substring(5, 7), 1);

}
}",
"reduce": "function(keys, values, rereduce) { return sum(values); }"
}
With that in place, I need to update the before(:each) example code block for the Haml specs:
  before(:each) do
assigns[:meals] = {
'rows' => [
{ "value" => [{"date" => '2009-05-14', "title" => 'Meal 1'}]},
{ "value" => [{"date" => '2009-05-15', "title" => 'Meal 2'}]},

]
}
assigns[:year] = 2009
assigns[:month] = '05'
assigns[:count_by_year] = [{"key" => "2009-04", "value" => 3},
{"key" => "2009-05", "value" => 3}]
end
The bits in bold were changed to look like CouchDB records. With that passing, I specify that the meals-by-month listing should include the meal date and summary:
  it "should include each meal's date in the title" do
render("/views/meal_by_month.haml")
response.should have_selector("h2", :content => "2009-05-14")
end

it "should include each meal's summary" do
render("/views/meal_by_month.haml")
response.should have_selector("p", :content => "Meal 2 Summary")
end
After padding the before(:each) block with data for both of these, I get the examples passing by modifying the Haml view:
%h1= "Meals from #{@year}-#{@month}"

.meals
- @meals["rows"].each do |meal|
%h2
%span.date= meal['value'][0]['date']
%span.title= meal['value'][0]['title']
%p= meal['value'][0]['summary']
I am also going to want to include the menu items and a meal thumbnail. Both of these are going to be tricky, so I'll defer that for another day. So that I do not forget, I add some reminders in the Haml spec:
  it "should include a thumbnail image of the meal"

it "should include the menu items"

it "should include recipe titles in the menu items"
Before calling it a day (or moving on to those pending specs), I work my way back out to the Cucumber senario. I have done enough work that I should be able to check off one or two more steps. This is also a good time to test the complete stack of Sinatra, Haml, and CouchDB. The entire scenario:
  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
Already done is viewing the list of meals in May of 2009. Now I can verify that the meal from May of 2009 is included and that the meal from April of 2009 is not by implementing the two steps:
Then /^I should see the "([^\"]*)" meal among the meals of this month$/ do |title|
response.should have_selector("h2", :content => title)
end

Then /^I should not see the "([^\"]*)" meal among the meals of this month$/ do |title|
response.should_not have_selector("h2", :content => title)
end
Thankfully, for once, Cucumber proves that I have gotten this assembled correctly.

Sunday, May 17, 2009

Another Link in the Chain, Another Bug Caught by Cucumber

‹prev | My Chain | next›

I left off yesterday having gotten links to previous and next years' meals working. Next up is to work my way out from the detailed code back to the Cucumber scenario. If I have done my job well, the scenario will pass without changes...
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features/browse_meals.feature \
> -s "Browsing a meal in a given year"
Feature: Browse Meals

So that I can find meals made on special occasions
As a web user
I want to browse meals by date

Scenario: Browsing a meal in a given year
Given a "Even Fried, They Won't Eat It" meal enjoyed in 2009
And a "Salad. Mmmm." meal enjoyed in 2008
When I view the list of meals prepared in 2009
Then the "Even Fried, They Won't Eat It" meal should be included in the list
expected following output to contain a <li a>Even Fried, They Won't Eat It</li a> 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></ul>
</body></html>
(Spec::Expectations::ExpectationNotMetError)
features/browse_meals.feature:12:in `Then the "Even Fried, They Won't Eat It" meal should be included in the list'
And the "Salad. Mmmm." meal should not be included in the list
When I follow the link to the list of meals in 2008
Then the "Even Fried, They Won't Eat It" meal should not be included in the list
And the "Salad. Mmmm." meal should be included in the list

1 scenario
1 failed step
4 skipped steps
3 passed steps
Ah, nuts.

Ooh, I forgot to add the type to the meal in the Given step. The CouchDB meals views are predicated on this attribute, so adding it ought to resolve the trouble (in bold):
Given /^a "([^\"]*)" meal enjoyed in (\d+)$/ do |title, year|
date = Date.new(year.to_i, 5, 13)

permalink = "id-#{date.to_s}"

meal = {
:title => title,
:date => date.to_s,
:serves => 4,
:summary => "meal summary",
:description => "meal description",
:type => "Meal"
}

RestClient.put "#{@@db}/#{permalink}",
meal.to_json,
:content_type => 'application/json'
end
Well, that fixes the above error, but the 2008 link is still not showing:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features/browse_meals.feature \
-s "Browsing a meal in a given year"
Feature: Browse Meals

So that I can find meals made on special occasions
As a web user
I want to browse meals by date

Scenario: Browsing a meal in a given year
Given a "Even Fried, They Won't Eat It" meal enjoyed in 2009
And a "Salad. Mmmm." meal enjoyed in 2008
When I view the list of meals prepared in 2009
Then the "Even Fried, They Won't Eat It" meal should be included in the list
And the "Salad. Mmmm." meal should not be included in the list
When I follow the link to the list of meals in 2008
Could not find link with text or title or id "2008" (Webrat::NotFoundError)
features/browse_meals.feature:14:in `When I follow the link to the list of meals in 2008'
Then the "Even Fried, They Won't Eat It" meal should not be included in the list
And the "Salad. Mmmm." meal should be included in the list

1 scenario
1 failed step
2 skipped steps
5 passed steps
After some quick print $stderr debugging (it's how real programmers debug), I find that the Sinatra app was not parsing the JSON returned from the CouchDB view:
get %r{/meals/(\d+)} do |year|
url = "#{@@db}/_design/meals/_view/by_year?group=true&key=%22#{year}%22"
data = RestClient.get url
@meals = JSON.parse(data)
@year = year

url = "#{@@db}/_design/meals/_view/count_by_year?group=true"
data = RestClient.get url
@count_by_year = data['rows']

haml :meal_by_year
end
Ah geez, I hate having to to spec JSON parsing (twice). Then again, if I had not skipped that in the first place, I would not have hit a problem here. I also could have avoided this by using CouchRest to manage the JSON <=> Ruby serialization. If I mess up like this again, I will have to switch. I may end up switching anyway.

I choose not to spec the JSON parsing. I would not be testing behavior, just implementation. Besides, I would have to jump through the same kind of hoops that I had to the other night for RestClient calls. The updated implementation:
  url = "#{@@db}/_design/meals/_view/count_by_year?group=true"
data = RestClient.get url
@count_by_year = JSON.parse(data)['rows']
With that, the scenario fails in exactly the same way!
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features/browse_meals.feature \
-s "Browsing a meal in a given year"
Feature: Browse Meals

So that I can find meals made on special occasions
As a web user
I want to browse meals by date

Scenario: Browsing a meal in a given year
Given a "Even Fried, They Won't Eat It" meal enjoyed in 2009
And a "Salad. Mmmm." meal enjoyed in 2008
When I view the list of meals prepared in 2009
Then the "Even Fried, They Won't Eat It" meal should be included in the list
And the "Salad. Mmmm." meal should not be included in the list
When I follow the link to the list of meals in 2008
Could not find link with text or title or id "2008" (Webrat::NotFoundError)
features/browse_meals.feature:14:in `When I follow the link to the list of meals in 2008'
Then the "Even Fried, They Won't Eat It" meal should not be included in the list
And the "Salad. Mmmm." meal should be included in the list

1 scenario
1 failed step
2 skipped steps
5 passed steps
How badly did I mess up last night?

This failure is caused by my implementation of the link to the next year's meals:
    def link_to_next_year(current, couch_view)
next_result = couch_view.detect do |result|
result['key'].to_i > current.to_i
end
if next_result
%Q|<a href="/meals/#{next_result['key']}">#{next_result['key']}</a>|
else
""
end
end
Somehow, I thought this would work both for the next and previous years—the latter by reversing the list of years. That does not work because, even with the years reversed when sending them to this helper, I am still asking for a year that is greater than the current year. I was so badly confused that I am not even sure how much sense that makes. What I expected to happen was this:
describe "link_to_next_year" do
before(:each) do
@count_by_year = [{"key" => "2008", "value" => 3},
{"key" => "2009", "value" => 3}]
end
it "should link to the previous before the current one" do
link_to_next_year(2009, @count_by_year.reverse).
should have_selector("a",
:href => "/meals/2008")
end
But that fails with no output.

To get this working I need to tell link_to_next_year to link to the previous year as needed. First off, I will rename the function to link_to_year_in_set. The default should be the subsequent year, but and option should link to the previous year.

I re-work the implementation such that the :previous option drives the detection Proc used as well as the reversal of the set:
    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
With that fixed (and all of the code updated to use the new helper), I give the Cucumber scenario one last try and find it working as expected:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features/browse_meals.feature \
> -s "Browsing a meal in a given year"
Feature: Browse Meals

So that I can find meals made on special occasions
As a web user
I want to browse meals by date

Scenario: Browsing a meal in a given year
Given a "Even Fried, They Won't Eat It" meal enjoyed in 2009
And a "Salad. Mmmm." meal enjoyed in 2008
When I view the list of meals prepared in 2009
Then the "Even Fried, They Won't Eat It" meal should be included in the list
And the "Salad. Mmmm." meal should not be included in the list
When I follow the link to the list of meals in 2008
Then the "Even Fried, They Won't Eat It" meal should not be included in the list
And the "Salad. Mmmm." meal should be included in the list

1 scenario
8 passed steps
The lesson to learn from tonight is that Cucumber scenarios can really save you—even when you think you are implementing something relatively simple.
(commit)

Saturday, May 16, 2009

Between Years

‹prev | My Chain | next›

Up next in the browsing meals by year scenario is navigating between years:



Nice! That step is easy to implement thanks to webrat:
When /^I follow the link to the list of meals in 2008$/ do
click_link "2008"
end
That fails, of course, because I have yet to add links to previous / next years. So let's do that (and account for boundary conditions while we're at it). Into the code...

When the user is looking at 2008 meals, the application needs a count of the meals in 2007 and 2009 to whether or not to link to them. I could load in all the meals from both years using the same view that lists all meals in a given year and then do a count on the results or I can just ask CouchDB to do the count itself:
    "count_by_year": {
"map": "function (doc) {
if (doc['type'] == 'Meal') {
emit(doc['date'].substring(0, 4), 1);
}
}",
"reduce": "function(keys, values, rereduce) { return sum(values); }"
}
Describing how the /meals/YYYY action will use this view:
      it "should ask CouchDB how many meals by year" do
RestClient.
should_receive(:get).
with(/meals.+count_by_year/).
and_return('{"rows": [] }')

get "/meals/2009"
end
To use the results of the view to display links to the next / previous years' worth of meals, I will use a helper. Getting my BDD on:
describe "link_to_next_year" do
before(:each) do
@count_by_year = [{"key" => "2008", "value" => 3},
{"key" => "2009", "value" => 3}]
end
it "should link to the next year after the current one" do
link_to_next_year(@current_year, 2008).
should have_selector("a",
:href => "/meals/2009")
end
end
I can get this example passing by detecting the next year after the current one:
    def link_to_next_year(current, couch_view)
next_result = couch_view.detect do |result|
result['key'].to_i > current
end
%Q|<a href="/meals/#{next_result['key']}">#{next_result['key']}</a>|
end
Accounting for boundary conditions, when there are no more results, no link should be rendered:
  it "should return empty if there are no more years" do
link_to_next_year(2009, @count_by_year).
should == ""
end
This example can be made to pass by adding a conditional to the earlier definition of link_to_next_year:
    def link_to_next_year(current, couch_view)
next_result = couch_view.detect do |result|
result['key'].to_i > current.to_i
end
if next_result
%Q|<a href="/meals/#{next_result['key']}">#{next_result['key']}</a>|
else
""
end
end
That is a good stopping point for tonight. I will work my way back out to the feature tomorrow.

Wednesday, May 13, 2009

Browsing Meals

‹prev | My Chain | next›

Up next, according to Cucumber (my master), is the "Browse Meals" feature. The first scenario in there is "Browsing a meal in a given year":
  Scenario: Browsing a meal in a given year
Given a "Even Fried, They Won't Eat It" meal enjoyed in 2009
When I view the list of meals prepared in 2009
Then "Even Fried, They Won't Eat It" should be included in the list

Given a "Even Fried, They Won't Eat It" meal enjoyed in 2009
And a "Salad. Mmmm." meal enjoyed in 2008
When I view the list of meals prepared in 2009
Then I should be able to follow a link to the list of meals in 2008
And "Salad. Mmmm." should be included in the list
This was one of the first scenarios that I wrote and it shows, I think. I have the same "Given" and the order of the steps reads a bit off. Re-organizing the scenario a bit:
  Scenario: Browsing a meal in a given year

Given a "Even Fried, They Won't Eat It" meal enjoyed in 2009
And a "Salad. Mmmm." meal enjoyed in 2008
When I view the list of meals prepared in 2009
Then the "Even Fried, They Won't Eat It" meal should be included in the list
And the "Salad. Mmmm." meal should not be included in the list
When I follow the link to the list of meals in 2008
Then the "Even Fried, They Won't Eat It" meal should not be included in the list
And the "Salad. Mmmm." meal should be included in the list
Much better.

My "Givens" are declared at the outset. There are two paths being followed (one for 2009, one for 2008), both starting with "When" declarations. Best of all, the second "When" flows from the first—clicking a link displayed in the first.

Implementing the first two steps can be accomplished via a single Given block:
Given /^a "([^\"]*)" meal enjoyed in (\d+)$/ do |title, year|
date = Date.new(year.to_i, 5, 13)

permalink = "id-#{date.to_s}"

meal = {
:title => title,
:date => date,
:serves => 4,
:summary => "meal summary",
:description => "meal description"
}

RestClient.put "#{@@db}/#{permalink}",
meal.to_json,
:content_type => 'application/json'
end
The next step, viewing the list of meals in 2009, can be defined as:
When /^I view the list of meals prepared in 2009$/ do
visit("/meals/2009")
response.status.should == 200
end
It fails, of course, since I have to define the meals action, so into the code I go...

As with recipes, I write my meal creation / tear down before / after blocks (this ain't no relational DB with fancy transactions):
  context "a CouchDB meal" do
before(:each) do
@date = Date.new(2009, 5, 13)
@title = "Meal Title"
@permalink = "id-#{@date.to_s}"

meal = {
:title => @title,
:date => @date,
:serves => 4,
:summary => "meal summary",
:description => "meal description"
}

RestClient.put "#{@@db}/#{@permalink}",
meal.to_json,
:content_type => 'application/json'

end

after(:each) do
data = RestClient.get "#{@@db}/#{@permalink}"
meal = JSON.parse(data)

RestClient.delete "#{@@db}/#{@permalink}?rev=#{meal['_rev']}"
end
end
The first meals action example is a simple one:
    describe "GET /meals/YYYY" do
it "should respond OK" do
get "/meals/2009"
response.should be_ok
end
end
This fails, of course since the action has not been defined. Let's define it and call it a night:
get %r{/meals/(\d+)} do |year|
end
(commit)
(commit)

Three steps down, 5 to go in the first meal scenario:



Looks like some fun with map-reduce is in store for tomorrow!

Tuesday, May 5, 2009

Cleaning Up a Mess of My Own Making

‹prev | My Chain | next›

First up today is cleaning up some of the mess that I left behind. When I run all of the Cucumber scenarios, I see many failing steps now:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features -s "Paginating results"
Feature: Search for recipes

So that I can find one recipe among many
As a web user
I want to be able search recipes

Scenario: Paginating results
Given 50 yummy recipes
And a 0.5 second wait to allow the search index to be updated
When I search for "yummy"
Then I should see 20 results
expected following output to contain a <table td a/> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<table><tr>
<th>
<a href="/recipes/search?q=yummy&sort=sort_title" id="sort-by-name">Name</a>
</th>
<th>
<a href="/recipes/search?q=yummy&sort=sort_date&order=desc" id="sort-by-date">Date</a>
</th>
<th>
<a href="/recipes/search?q=yummy&sort=sort_prep" id="sort-by-prep">Prep</a>
</th>
<th>
<a href="/recipes/search?q=yummy&sort=sort_ingredient" id="sort-by-ingredients">Ingredients</a>
</th>
</tr></table>
<div class="pagination">
<span class="inactive">« Previous</span><a href="/recipes/search?q=yummy&page=2">Next »</a>
</div>
</body></html>
(Spec::Expectations::ExpectationNotMetError)
features/recipe_search.feature:57:in `Then I should see 20 results'
Hunh? This step passed previously—there were plenty of matching recipes before, so where did they go?

The answer is in the CouchDB log:
[couchdb-lucene] ERROR Error updating index.
org.mozilla.javascript.EcmaError: TypeError: Cannot read property "length" from undefined (userFun#38)
at org.mozilla.javascript.ScriptRuntime.constructError(ScriptRuntime.java:3557)
at org.mozilla.javascript.ScriptRuntime.constructError(ScriptRuntime.java:3535)
...
Ah, this is coming from sort-by-number-of-ingredients indexed field:
  ret.field('sort_ingredient', doc['preparations'].length, 'yes', 'not_analyzed');
This is being tripped when there are no ingredient preparation fields—something that might happen when drafting a recipe, so it is worth fixing:
  var ingredient_count = doc['preparations'] ? doc['preparations'].length : 0;
ret.field('sort_ingredient', ingredient_count, 'yes', 'not_analyzed');
Even with that fixed, the scenario is still not passing:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features -s "Paginating results"
Feature: Search for recipes

So that I can find one recipe among many
As a web user
I want to be able search recipes

Scenario: Paginating results
Given 50 yummy recipes
And a 0.5 second wait to allow the search index to be updated
When I search for "yummy"
Then I should see 20 results
And I should see 3 pages of results
And I should not be able to go to a previous page
When I click page 3
Then I should see 10 results
expected following output to contain a <table td a/> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<table>
<tr>
<th>
<a href="/recipes/search?q=yummy&sort=sort_title" id="sort-by-name">Name</a>
</th>
<th>
<a href="/recipes/search?q=yummy&sort=sort_date&order=desc" id="sort-by-date">Date</a>
</th>
<th>
<a href="/recipes/search?q=yummy&sort=sort_prep" id="sort-by-prep">Prep</a>
</th>
<th>
<a href="/recipes/search?q=yummy&sort=sort_ingredient" id="sort-by-ingredients">Ingredients</a>
</th>
</tr>
<tr class="row0">
<td>
<a href="/recipes/id-40-yummy">yummy recipe 40</a>
</td>
<td>
<span class="date">2009-04-22</span>
</td>
<td>
<span class="prep">0</span>
</td>
<td>
<span class="ingredients"></span>
</td>
</tr>
<tr class="row1">
...
Counting out the number of rows in the results I find eleven, not the expected 10. My fencepost fix from the other night uncovered an accidental step passage tonight. The fix is an easy one. Easy, but my mistake should have been obvious from the start. In the recipe creation step, I create recipes in the range 0..50—51 including the zero. Changing it to 1..50, all specs and all Cucumber scenarios are passing:



(commit)

There still remains some work in the Recipe Search scenario: (1) No matching results and (2) Invalid search parameters.

Most of the "No matching results" scenario steps are already implemented:



All that remains is verifying that no results are displayed. The two scenario steps can be defined as:
Then /^I should see no results$/ do
response.should have_selector("p.no-results")
end

Then /^no result headings$/ do
response.should_not have_selector("th")
end
I can describe the desired behavior in the Sinatra app as:
    it "should display a helpful message when no results" do
RestClient.stub!(:get).
and_return('{"total_rows":0,"skip":0,"limit":20,"rows":[]}')

get "/recipes/search?q=title:egg"

response.should contain("No results")
end
To make this example pass, a simple ternary will suffice:
  haml @results['total_rows'] == 0 ? :no_results : :search
Well, that, plus a simple Haml template suffice to get things working.

With that, I have another scenario complete and only one more to go in the recipe search feature. Even better, I have exactly 100 passing steps!



(commit)

Sunday, May 3, 2009

Sorting by Date (Non-Default Order)

‹prev | My Chain | next›

Note: date sorting as described here only works when using ISO 8601 date format.

I have sorting and pagination playing nicely together at this point:



Up next is sorting by date. It is treated differently than other sorting because the default sort order is descending rather than ascending. By default, the newest recipes should be shown (descending order). All other sorting should be in alphabetically ascending order ("a" comes before "b", "b" comes before "c", etc.).

The "Then" step describing the first page of descending date results will include "2008-06-17" and "2008-06-16" (day 50 and 49, as measured from day 1, "2008-04-29"). Thanks to Cucumber / Webrat, this is easy to describe as:
Then /^the results should be ordered by date in descending order$/ do
response.should have_selector("tr:nth-child(2) .date",
:content => "2008-06-17")
response.should have_selector("tr:nth-child(3) .date",
:content => "2008-06-16")
end
As expected, this step fails, so it is time to wade into the code to get it working as desired.

In order to instruct the sort_link helper to reverse by default, I add an optional 4th parameter:
  it "should link to descending sort if instructed to reverse" do
sort_link("Foo",
"sort_foo",
@current_results,
:query => "query",
:reverse => true).
should have_selector("a",
:href => "/recipes/search?q=query&sort=sort_foo&order=desc")
end
I also make the query part of the optional argument hash (since it is included, albeit stemmed, in the couchdb-lucene results). The implementation that makes this and other examples work:
    def sort_link(text, sort_field, results, options = { })
id = "sort-by-#{text.downcase}"

query = options[:query] || results['query']

# Current state of sort on the requested field
sort_field_current =
results["sort_order"] &&
results["sort_order"].detect { |sort_options|
sort_options["field"] == sort_field
}

if sort_field_current
order = sort_field_current["reverse"] ? "" : "&order=desc"
elsif options[:reverse]
order = "&order=desc"
end

url = "/recipes/search?q=#{query}&sort=#{sort_field}#{order}"
%Q|<a href="#{url}" id="#{id}">#{text}</a>|
end
With descending by default date sorting working properly, I can work my way back out to the feature description, which is now passing. Implementing a similar step for clicking the "Date" column header twice gets me down to the last 4 steps in the search scenario:



Almost there.

Saturday, May 2, 2009

Sorting, Page 2

‹prev | My Chain | next›

With sorting and reverse sorting working, up next is propagating that sorting through pagination.

The setup for the existing pagination helper specs reads:
describe "pagination" do
before(:each) do
@query = 'foo'
@results = { 'total_rows' => 41, 'limit' => 20, 'skip' => 0}
end
...
end
To describe pagination with sorting, I add a new context to the pagination description. The context adds a couchdb-lucene sort_order attribute to the results set from the parent pagination block:
  context "with sorting applied" do
before(:each) do
@results["sort_order"] = [{ "field" => "sort_foo",
"reverse" => false}]
end
...
end
With that, an example of a link to a second page of sorted results looks like:
    it "should have a link to other pages with sorting applied" do
pagination(@query, @results).
should have_selector("a",
:content => "2",
:href => "/recipes/search?q=foo&sort=sort_foo&page=2")
end
To get that example working, I add a simple conditional to the pagination helper that adds the sort parameter under these conditions:
      if results['sort_order']
link += "&sort=#{results['sort_order'].first['field']}"
end
A similar example and conditional get reverse sorting working with pagination. Then it is back out to the Cucumber scenario, where I make a disturbing discovery.

In text order, the first 20 (of the 50 total in the Cucumber scenario) recipes in descending order are, "delicious recipe 9", "delicious recipe 8", 7, 6, 50, 5, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 4, 39, "delicious recipe 38", and "delicious recipe 37". The first two on the next page should be "delicious recipe 36" and "delicious recipe 35", so I define this step to describe page 2 of descending title sorted results:
Then /^the results should be ordered by name in descending order$/ do
response.should have_selector("tr:nth-child(2) a",
:content => "delicious recipe 36")
response.should have_selector("tr:nth-child(3) a",
:content => "delicious recipe 35")
Sadly, when I run the cucumber scenario, I have skipped 36:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features -s "Sorting (name, date, preparation time, number of ingredients)"
Feature: Search for recipes

So that I can find one recipe among many
As a web user
I want to be able search recipes

Scenario: Sorting (name, date, preparation time, number of ingredients)
Given 50 "delicious" recipes with ascending names, dates, preparation times, and number of ingredients
And a 0.5 second wait to allow the search index to be updated
When I search for "delicious"
Then I should see 20 results
When I click the "Name" column header
Then the results should be ordered by name in ascending order
When I click the "Name" column header
Then the results should be ordered by name in descending order
When I click the next page
Then I should see page 2
And the results should be ordered by name in descending order
expected following output to contain a <tr:nth-child(2) a>delicious recipe 36</tr:nth-child(2) a> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<table>
<tr>
<th>
<a href="/recipes/search?q=delicious&sort=sort_title" id="sort-by-name">Name</a>
<a href="/recipes/search?q=delicious&sort=sort_date" id="sort-by-date">Date</a>
</th>
<th>Date</th>
</tr>
<tr class="row0">
<td>
<a href="/recipes/id-35-delicious">delicious recipe 35</a>
</td>
<td>2008-06-02</td>
</tr>
<tr class="row1">
<td>
<a href="/recipes/id-34-delicious">delicious recipe 34</a>
</td>
<td>2008-06-01</td>
</tr>
...
Damn. A fence post problem. I added a one to the couchdb-lucene "skip" parameter that was not needed:
  skip = (page < 2) ? 0 : ((page - 1) * 20) + 1
Removing the 1 removes the fencepost from the calculation and fixes the Cucumber scenario, but requires a bit of unit-level spec clean-up.
(commit)

This was a legit bug that I introduced. If I had not had the Cucumber integration test, I would have introduced a bug into live code, so outside-in testing really saved me today!

Thursday, April 30, 2009

Down the Sort Hole

‹prev | My Chain | next›

With an understanding between me and couchdb-lucene sorting, I start back with implementation. In the Sinatra application's spec for /recipes/search, I add:
    it "should sort" do
RestClient.should_receive(:get).
with(/sort=title/).
and_return('{"total_rows":30,"skip":0,"limit":20,"rows":[]}')

get "/recipes/search?q=title:egg&sort=title"
end
I make this example pass by simply passing the sort parameter through to couchdb-lucene:
data = RestClient.get "#{@@db}/_fti?limit=20&skip=#{skip}&q=#{params[:q]}&sort=#{params[:sort]}"
That spec may pass, but my cucumber scenario no longer does:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features \
-s "Sorting (name, date, preparation time, number of ingredients)"
Feature: Search for recipes

So that I can find one recipe among many
As a web user
I want to be able search recipes

Scenario: Sorting (name, date, preparation time, number of ingredients)
Given 50 "delicious" recipes with ascending names, dates, preparation times, and number of ingredients
And a 0.5 second wait to allow the search index to be updated
When I search for "delicious"
HTTP status code 400 (RestClient::RequestFailed)
/usr/lib/ruby/1.8/net/http.rb:543:in `start'
./features/support/../../eee.rb:30:in `GET /recipes/search'
(eval):7:in `get'
features/recipe_search.feature:79:in `When I search for "delicious"'
Then I should see 20 results
When I click the "Name" column header
...
The cucumber scenario is not even reaching the sorting steps—it is failing on the simple search-for-a-string step. The cause of the failure is couchdb-lucene's dislike of empty (or non-indexed) sort fields. I have to guard against empty sort parameters:
    it "should not sort when no sort field is supplied" do
RestClient.stub!(:get).
and_return('{"total_rows":30,"skip":0,"limit":20,"rows":[]}')

RestClient.should_not_receive(:get).with(/sort=/)

get "/recipes/search?q=title:egg&sort="
end
I can implement this example thusly:
get '/recipes/search' do
@query = params[:q]

page = params[:page].to_i
skip = (page < 2) ? 0 : ((page - 1) * 20) + 1

couchdb_url = "#{@@db}/_fti?limit=20" +
"&q=#{@query}" +
"&skip=#{skip}"

if params[:sort] =~ /\w/
couchdb_url += "&sort=#{params[:sort]}"
end

data = RestClient.get couchdb_url

@results = JSON.parse(data)

if @results['rows'].size == 0 && page > 1
redirect("/recipes/search?q=#{@query}")
return
end

haml :search
end
With that, my Cucumber scenarios are again passing and I am ready to proceed with the view / helper work.
(commit)

Shortly after starting work in the Haml template, the sort field gets unwieldy, which is a good indication that it ought to be a helper. I opt for the name of sort_link for the helper and build the following examples to describe how it should work:
describe "sort_link" do
it "should link the supplied text" do
sort_link("Foo", "sort_foo", "query").
should have_selector("a",
:content => "Foo")
end
it "should link to the query with the supplied sort field" do
sort_link("Foo", "sort_foo", "query").
should have_selector("a",
:href => "/recipes/search?q=query&sort=sort_foo")
end
end
I implement this code as:
    def sort_link(text, sort_on, query)
id = "sort-by-#{text.downcase}"
url = "/recipes/search?q=#{query}&sort=#{sort_on}"
%Q|#{text}|
end
There are no example for the link's id. That is semantic information, having nothing to do with behavior of the application. The only reason to include it is for styling and, more importantly, the Cucumber scenario.

Speaking of the Cucumber scenario, I am now ready to implement the next step, Then the results should be ordered by name in ascending order, which is aided by some CSS selector fanciness:
Then /^the results should be ordered by name in ascending order$/ do
response.should have_selector("tr:nth-child(2) a",
:content => "delicious recipe 1")
response.should have_selector("tr:nth-child(3) a",
:content => "delicious recipe 10")
end
The first child of the results table is the header, which is the reason the first selector is looking for the second child. The reason for the second test is that I want to ensure that sorting has taken place. The "delicious recipe 1" was the first recipe entered, so it may show up in the results list first for that reason alone. But "delicious recipe 10" will come before "delicious recipe 2" only if they have been sorted (because the "1" in "10" comes before "2" when performing text sorting).
(commit)

Up next: reversing the sort order.

Friday, April 24, 2009

Pagination, Page 3

‹prev | My Chain | next›

Continuing pagination work, I need to get previous and next links working. I also never actually put links in the a tags, so I will get that working as well.

To get the href working, I add an href expectation to the first pagination expectation from last night:
  it "should have a link to other pages" do
pagination('foo', 0, 20, 41).
should have_selector("a",
:content => "2",
:href => "/recipes/search?q=foo&page=2")
end
To drive development of the next / previous link, I write the following examples:
  it "should have a link to the next page if before the last page" do
pagination('foo', 20, 20, 41).
should have_selector("a", :content => "Next »")
end
it "should not have a link to the next page if on the last page" do
pagination('foo', 40, 20, 41).
should have_selector("span", :content => "Next »")
end
it "should have a link to the previous page if past the first page" do
pagination('foo', 20, 20, 41).
should have_selector("a", :content => "« Previous")
end
it "should not have a link to the next page if on the first page" do
pagination('foo', 0, 20, 41).
should have_selector("span", :content => "« Previous")
end
Working through each of these examples, I end up with the following, longish implementation:
    def pagination(query, skip, limit, total)
last_page = (total + limit - 1) / limit
current_page = skip / limit + 1

link = "/recipes/search?q=#{query}"

links = []

links <<
if current_page == 1
%Q|<span class="inactive">« Previous</span>|
else
%Q|<a href="#{link}&page=#{current_page - 1}">« Previous</a>|
end

links << (1..last_page).map do |page|
%Q|<a href="#{link}&page=#{page}">#{page}</a>|
end

links <<
if current_page == last_page
%Q|<span class="inactive">Next »</span>|
else
%Q|<a href="#{link}&page=#{current_page + 1}">Next »</a>|
end

%Q|<div class="pagination">#{links.join}</div>|
end
I can DRY that up some, especially the conditionals around the previous / next links. For now, I will get it done and worry about doing it right another day. At least the repetition is small and all contained within a single method.

The addition of the query parameter to the pagination helper requires a bunch of clean-up in both specs and code. Once that is done, it is back on out to the cucumber feature:
    Scenario: Paginating results

Given 50 yummy recipes
And a 0.5 second wait to allow the search index to be updated
When I search for "yummy"
Then I should see 20 results
And I should see 3 pages of results
And I should not be able to go to a previous page
When I click page 3
Then I should see 10 results
And I should not be able to go to a next page
When I click the previous page
Then I should see 20 results
And I should be able to go to a previous page
When I click the next page
Then I should see 10 results
When I visit page -1
Then I should see page 1
When I visit page "foo"
Then I should see page 1
When I visit page 4
Then I should see page 1
To verify that I should not be able to go to a previous page, I use the following:
Then /^I should not be able to go to a previous page$/ do
response.should have_selector(".pagination span", :content => "« Previous")
end
Running the spec, I find my final failure for today:
cstrom@jaynestown:~/repos/eee-code$ cucumber features/recipe_search.feature -n -s "Paginating results"
Feature: Search for recipes

So that I can find one recipe among many
As a web user
I want to be able search recipes

Scenario: Paginating results
Given 50 yummy recipes
And a 0.5 second wait to allow the search index to be updated
When I search for "yummy"
Then I should see 20 results
And I should see 3 pages of results
And I should not be able to go to a previous page
When I click page 3
Then I should see 10 results
expected following output to contain a <table a/> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<table>
<tr>
<th>Name</th>
<th>Date</th>
</tr>
<tr class="row0">
<td>
<a href="/recipes/id-0-yummy">yummy recipe 0</a>
</td>
<td>2009-04-22</td>
</tr>
<tr class="row1">
<td>
<a href="/recipes/id-1-yummy">yummy recipe 1</a>
</td>
<td>2009-04-22</td>
</tr>
<tr class="row0">
...
Still on recipe 0? Oops, I have yet to connect the page parameters to the RestClient calls to couchdb-lucene.

I do not mind stopping with a failing test. Quite the opposite, I know exactly where to start tomorrow.

Sunday, April 19, 2009

Field Search: Searching on titles

‹prev | My Chain | next›

The next scenario up is "Searching titles", which is described in Cucumber as:
    Scenario: Searching titles

Given a "pancake" recipe
And a "french toast" recipe with a "not a pancake" summary
And a 0.25 second wait to allow the search index to be updated
When I search titles for "pancake"
Then I should see the "pancake" recipe in the search results
And I should not see the "french toast" recipe in the search results
The Given a-recipe-with-summary step already has a step definition. The Given a-recipe-with-a-title step needs a definition:
Given /^a "(.+)" recipe$/ do |title|
date = Date.new(2009, 4, 19)
permalink = "id-#{title.gsub(/\W/, '-')}"

recipe = {
:title => title,
:date => date,
}

RestClient.put "#{@@db}/#{permalink}",
recipe.to_json,
:content_type => 'application/json'
end
The next step is When I search titles for "pancake", which can be defined as:
When /^I search titles for "(.+)"$/ do |keyword|
visit("/recipes/search?q=title:#{keyword}")
end
The only difference between this and the already defined When I search for "foo" is the addition of the title query parameter. Attempting to run this query, however results in a brutal RestClient failure:
cstrom@jaynestown:~/repos/eee-code$ cucumber features/recipe_search.feature -n \
-s "Searching titles"
Feature: Search for recipes

So that I can find one recipe among many
As a web user
I want to be able search recipes
Scenario: Searching titles
Given a "pancake" recipe
And a "french toast" recipe with a "not a pancake" summary
And a 0.25 second wait to allow the search index to be updated
When I search titles for "pancake"
HTTP status code 400 (RestClient::RequestFailed)
/home/cstrom/.gem/ruby/1.8/gems/rest-client-0.9.2/lib/restclient/request.rb:144:in `process_result'
/home/cstrom/.gem/ruby/1.8/gems/rest-client-0.9.2/lib/restclient/request.rb:106:in `transmit'
/usr/lib/ruby/1.8/net/http.rb:543:in `start'
/home/cstrom/.gem/ruby/1.8/gems/rest-client-0.9.2/lib/restclient/request.rb:103:in `transmit'
/home/cstrom/.gem/ruby/1.8/gems/rest-client-0.9.2/lib/restclient/request.rb:36:in `execute_inner'
/home/cstrom/.gem/ruby/1.8/gems/rest-client-0.9.2/lib/restclient/request.rb:28:in `execute'
/home/cstrom/.gem/ruby/1.8/gems/rest-client-0.9.2/lib/restclient/request.rb:12:in `execute'
/home/cstrom/.gem/ruby/1.8/gems/rest-client-0.9.2/lib/restclient.rb:57:in `get'
./features/support/../../eee.rb:20:in `GET /recipes/search'
/home/cstrom/.gem/ruby/1.8/gems/sinatra-0.9.1.1/lib/sinatra/base.rb:696:in `call'
...
(continues for quite a while)
RestClient errors warrant a peak in the CouchDB log, where I find:
[info] [<0.3573.3>] 127.0.0.1 - - 'GET' /eee-test/_fti?q=all:title:pancake 400
We are getting an HTTP 400 / Bad Request response because the search itself is invalid. Lucene does fielded searches by prepending the field name to the search term, separated by a colon. Similar to how Google does it (e.g. "site:eeecooks.com spinach"), a lucene search for a recipe with the word "pancake" in the title would be searched for as "title:pancake". It makes no sense to smush two fields togther as we have, "all:title:pancake". Hence the 400 response.

It is probably a good thing that an invalid search returns an invalid (400) HTTP response code as opposed to some other code. Still, I should investigate a bit more later, so I make a note for myself to do so in the form of a step-less scenario:
    Scenario: Invalid search parameters
Getting back to the current failing step, it is time to move inside the feature.

A second example for "/recipes/search" will describe the new, desired behavior:
    it "should not include the \"all\" field when performing fielded searches" do
RestClient.should_receive(:get).
with("#{@@db}/_fti?q=title:eggs").
and_return('{"total_rows":1,"rows":[]}')

get "/recipes/search?q=title:eggs"
end
The original example is only slightly different, defaulting to the "all" field that we are using to index entire documents:
    it "should retrieve search results from couchdb-lucene" do
RestClient.should_receive(:get).
with("#{@@db}/_fti?q=all:eggs").
and_return('{"total_rows":1,"rows":[]}')

get "/recipes/search?q=eggs"
end
The first time I run the spec, the new example fails:
cstrom@jaynestown:~/repos/eee-code$ spec ./spec/eee_spec.rb
....F

1)
Spec::Mocks::MockExpectationError in 'eee GET /recipes/search should not include the "all" field when performing fielded searches'
RestClient expected :get with ("http://localhost:5984/eee-test/_fti?q=title:eggs") but received it with ("http://localhost:5984/eee-test/_fti?q=all:title:eggs")
./eee.rb:20:in `GET /recipes/search'
/home/cstrom/.gem/ruby/1.8/gems/sinatra-0.9.1.1/lib/sinatra/base.rb:696:in `call'
...
The easiest way to fix the error is to remove the double fields:
get '/recipes/search' do
query = "all:#{params[:q]}".sub(/(\w+):(\w+):/, "\\2:")
data = RestClient.get "#{@@db}/_fti?q=#{query}"
@results = JSON.parse(data)

haml :search
end
Now the specification passes:
cstrom@jaynestown:~/repos/eee-code$ spec ./spec/eee_spec.rb
.....

Finished in 0.079155 seconds

5 examples, 0 failures
With the inside, detailed specification passing, I try the outside specification and it works:
  So that I can find one recipe among many
As a web user
I want to be able search recipes
Scenario: Searching titles
Given a "pancake" recipe
And a "french toast" recipe with a "not a pancake" summary
And a 0.25 second wait to allow the search index to be updated
When I search titles for "pancake"
Then I should see the "pancake" recipe in the search results
And I should not see the "french toast" recipe in the search results


1 scenario
6 steps passed
I have some reservations about this particular simplest solution. The edge cases of parsing search queries are many. I will worry about that another day. Maybe even tomorrow.
(commit)

Wednesday, April 15, 2009

Inside-out with couchdb-lucene

‹prev | My Chain | next›

With couchdb-lucene returning data along with results, I get my red-green-refactor on tonight to finish implementing the first Recipe Search scenario.

My initial effort on this ended with the search action responding with a simple string. To get full output, a template is needed. The spec doc that I end up implementing is:
cstrom@jaynestown:~/repos/eee-code$ spec ./spec/views/search.haml_spec.rb  -cfs

search.haml
- should display the recipe's title
- should display a second recipe
- should display zebra strips
- should link the title to the recipe
- should display the recipe's date

Finished in 0.031609 seconds

5 examples, 0 failures
Check the commit if you are interested in the details of the individual specs. The Haml template that implements these 5 examples is still relatively simple at this point:
%table
%tr
%th= "Name"
%th= "Date"
- @results['rows'].each_with_index do |result, i|
%tr{:class => "row#{i % 2}"}
%td
%a{:href => "/recipes/#{result['_id']}"}= result['title']
%td= result['date']
Finally, working my way back out to the scenario I perform some accidental refactoring. The scenario that I need to implement is:
    Scenario: Matching a word in the ingredient list in full recipe search

Given a "pancake" recipe with "chocolate chips" in it
And a "french toast" recipe with "eggs" in it
And a 1 second wait to allow the search index to be updated
When I search for "chocolate"
Then I should see the "pancake" recipe in the search results
And I should not see the "french toast" recipe in the search results
The accidental refactoring took place in the first Then's definition. The original implementation was:
Then /^I should see the "pancake" recipe in the search results$/ do
response.should have_selector("a", :href => "/recipes/#{@pancake_permalink}")
end
The accidental refactoring took place when I misread the last Then statement to be in the same format as the first (I missed the addition of the word "not"). To work with both forms, the block-with-argument step definition works:
Then /^I should see the "(.+)" recipe in the search results$/ do |title|
response.should have_selector("a",
:href => "/recipes/id-#{title}",
:content => title)
end
Chagrined to see that the final step was still not implemented, I correct my omission, but leave the refactored definition in place. This is not a simple violation of YAGNI, because I am going to need it. Upcoming scenarios can use the generalized format. Still, I must be more careful before refactoring.
(commit)

Thursday, April 2, 2009

Recipe Details and the Kingdom of the Crystal Skull

‹prev | My Chain | next›

Today I work on the last of the recipe details scenarios, "Main site categories". At the top of each recipe page on the legacy EEE Cooks we list recipe categories (e.g. Italian, Vegetarian). If the recipe is in one of those categories, then the category is highlighted.

cstrom@jaynestown:~/repos/eee-code$ cucumber features/recipe_details.feature -n \
-s "Main site categories"
Feature: Recipe Details

So that I can accurately reproduce a recipe at home
As a web user
I want to be able to easily recognize important details
Scenario: Main site categories
Given a recipe for Mango and Tomato Salad
And site-wide categories of Italian, Asian, Latin, Breakfast, Chicken, Fish, Meat, Salad, and Vegetarian
When I view the recipe
Then the Salad and Vegetarian categories should be active


1 scenario
1 step skipped
3 steps pending (3 with no step definition)

You can use these snippets to implement pending steps which have no step definition:

Given /^a recipe for Mango and Tomato Salad$/ do
end

Given /^site\-wide categories of Italian, Asian, Latin, Breakfast, Chicken, Fish, Meat, Salad, and Vegetarian$/ do
end

Then /^the Salad and Vegetarian categories should be active$/ do
end
With 3 of 4 scenarios complete, I am in the groove. The recipe to implement the first Given step is:
  recipe = {
:title => @title,
:date => @date,
:tag_names => [ "vegetarian", "salad" ]
}
I rethink the second Given step as being better done as a Then step. If I had a Categories model, it could be a pre-condition. But in this application, it will just be links on the view—something that could described as "then I should see the site-wide categories".
(commit)

The view specs that I implement are:
  context "a vegetarian recipe" do
before(:each) do
@recipe['tag_names'] = ['vegetarian']
render("views/recipe.haml")
end
it "should highlight the vegetarian category at the top of the page" do
response.should have_selector("a",
:content => "Vegetarian",
:class => "active")
end
end

context "a vegetarian, italian recipe" do
before(:each) do
@recipe['tag_names'] = ['vegetarian', 'italian']
render("views/recipe.haml")
end
it "should highlight the vegetarian category at the top of the page" do
response.should have_selector("a",
:content => "Vegetarian",
:class => "active")
end
it "should highlight the italian category at the top of the page" do
response.should have_selector("a",
:content => "Italian",
:class => "active")
end
end
Implementation is relatively straight-forward. I use a helper to create the links in order to DRY things up a bit and to cut down on the ugly conditionals in the Haml template:
    def recipe_category_link(recipe, category)
if recipe['tag_names'] && recipe['tag_names'].include?(category.downcase)
%Q|<a class="active">#{category}</a>|
else
%Q|<a>#{category}</a>|
end
end
(commit)

Back out to the cucumber spec, the last two steps can be implemented thusly:
Then /^I should see the site\-wide categories of (.+)$/ do |category_list|
categories = category_list.
split(/\s*(,|and)\s*/).
reject{|str| str == "," || str == "and"}
response.should have_selector("#eee-categories") do |list|
categories.each do |category|
response.should have_selector("a", :content => category)
end
end
end

Then /^the Salad and Vegetarian categories should be active$/ do
response.should have_selector("a", :class => "active", :content => "Salad")
response.should have_selector("a", :class => "active", :content => "Vegetarian")
response.should_not have_selector("a", :class => "active", :content => "Fish")
end
I like throwing in a negative assertion like the Fish category should not be active. It is as if I were heckling my own code. Just like heckle, negative assertions are a nice, simple way to keep me honest.

Just like that I am done with the recipe details page. At some point I'll have to actually look at it. First though, I think I will have to get the image in there. When next my chain continues...