Showing posts with label views. Show all posts
Showing posts with label views. Show all posts

Tuesday, March 31, 2009

Accessing Sinatra Helper Methods in View Specs

‹prev | My Chain | next›

I left off last night pondering how to make Sinatra helpers available to Haml view specs. I tried digging into how helpers works in Sinatra, but was unable to discern a way to put any of it to use.

My ultimate solution is to break out my helper methods into a file named helpers.rb:
module Eee
module Helpers
def hours(minutes)
h = minutes.to_i / 60
m = minutes.to_i % 60
h > 0 ? "#{h} hours" : "#{m} minutes"
end
end
end
Requiring that file and then mixing it in inside the Sinatra helpers block gets me back to where I was last night:
require 'helpers'

helpers do
include Eee::Helpers
end
In order to make the helper methods available to my view specs, I configure Spec::Runner to include the helper module:
Spec::Runner.configure do |config|
config.include Webrat::Matchers, :type => :views
config.include Eee::Helpers
end
Finally, I change the default scope of the Haml rendering from a vanilla Object.new to self:
def render(template_path)
template = File.read("./#{template_path.sub(/^\//, '')}")
engine = Haml::Engine.new(template)
@response = engine.render(self, assigns_for_template)
end
Binding the render call to self (instances of Test::Unit sub-classes) gives the Haml engine an object that defines the helper methods (because they were included in the configuration block).

With those changes, I have all of my views specs passing, including displaying 5 hours instead of 300 minutes:
cstrom@jaynestown:~/repos/eee-code$ ruby ./spec/views/recipe.haml_spec.rb -cfs 

recipe.haml
- should display the recipe's title

recipe.haml a recipe with no ingredient preparations
- should not render an ingredient preparations

recipe.haml a recipe with 1 egg
- should render ingredient names
- should render ingredient quantities
- should not render a brand

recipe.haml a recipe with 1 cup of all-purpose, unbleached flour
- should include the measurement unit
- should include the specific kind of ingredient
- should read conversationally, with the ingredient kind before the name

recipe.haml a recipe with 1 12 ounce bag of Nestle Tollhouse chocolate chips
- should include the ingredient brand
- should note the brand parenthetically after the name

recipe.haml a recipe with an active and inactive preparation time
- should include preparation time
- should include inactive time

recipe.haml a recipe with no inactive preparation time
- should not include inactive time

recipe.haml a recipe with 300 minutes of inactive time
- should display 5 hours of Inactive Time

Finished in 0.063692 seconds

14 examples, 0 failures
(commit)

I may want to simply stub my helper method calls inside my view specs and test my helper methods independently of my views. Something to decide another day.

Sunday, March 29, 2009

Finishing Working Inside the Recipe Details, Moving Back Out

‹prev | My Chain | next›

Yesterday, I got in the flow of outside-in Behavior Driven Development. I reached a decidedly inside stopping point driving the development of my Haml views—even finding and addressing a boundary condition.

Work today is smoother as I add four more examples describing various display attributes. At this point, my specification describes the recipe.haml template as:
cstrom@jaynestown:~/repos/eee-code$ ruby ./spec/views/recipe.haml_spec.rb -cfs 

recipe.haml
- should display the recipe's title

recipe.haml a recipe with no ingredient preparations
- should not render an ingredient preparations

recipe.haml a recipe with 1 egg
- should render ingredient names
- should render ingredient quantities

recipe.haml a recipe with 1 cup of all-purpose, unbleached flour
- should include the measurement unit
- should include the specific kind of ingredient

recipe.haml a recipe with 1 12 ounce bag of Nestle Tollhouse chocolate chips
- should include the ingredient brand

Finished in 0.07205 seconds

7 examples, 0 failures
(commit)

I believe that I have completed all of the necessary implementation for the "Viewing a recipe with several ingredients" scenario. To remind myself of where I left off in the "outside" scenarios:
cstrom@jaynestown:~/repos/eee-code$ cucumber features/recipe_details.feature -n -s "Viewing a recipe with several ingredients"
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: Viewing a recipe with several ingredients
Given a recipe for Buttermilk Chocolate Chip Pancakes
When I view the recipe
Then I should see an ingredient of "1 cup of all-purpose, unbleached flour"
expected the following element's content to include "1 cup of all-purpose, unbleached flour":

Buttermilk Chocolate Chip Pancakes (Spec::Expectations::ExpectationNotMetError)
./features/step_definitions/recipe_details.rb:17:in `Then /^I should see an ingredient of "(.+)"$/'
features/recipe_details.feature:11:in `Then I should see an ingredient of "1 cup of all-purpose, unbleached flour"'
And I should see an ingredient of "¼ teaspoons salt"
And I should see an ingredient of "chocolate chips (Nestle Tollhouse)"

1 scenario
2 steps passed
1 step failed
2 steps skipped
The scenario is failing because it does not build any ingredient preparations. Adding them, and then training the "I should seen an ingredient" step to ignore whitespace, I still get a failing scenario:
cstrom@jaynestown:~/repos/eee-code$ cucumber features/recipe_details.feature -n -s "Viewing a recipe with several ingredients"
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: Viewing a recipe with several ingredients
Given a recipe for Buttermilk Chocolate Chip Pancakes
When I view the recipe
Then I should see an ingredient of "1 cup all-purpose, unbleached flour"
expected the following element's content to match /1\s+cup\s+all-purpose,\s+unbleached\s+flour/:

Buttermilk Chocolate Chip Pancakes
1
cup
flour
all-purpose, unbleached (Spec::Expectations::ExpectationNotMetError)
./features/step_definitions/recipe_details.rb:33:in `Then /^I should see an ingredient of "(.+)"$/'
features/recipe_details.feature:11:in `Then I should see an ingredient of "1 cup all-purpose, unbleached flour"'
And I should see an ingredient of "¼ teaspoons salt"
And I should see an ingredient of "chocolate chips (Nestle Tollhouse)"


1 scenario
2 steps passed
1 step failed
2 steps skipped
This is failing because the conversationally written spec (an ingredient of "1 cup all-purpose, unbleached flour") is not matching up with the order of implementation. As implemented, the ingredient name ("flour") comes before the kind of ingredient ("all-purpose, unbleached"), but the scenario expects them to be reversed. For ease of reading, conversationally written should win.

Back into the view specs...

Although it is not quite xpath, I need to rely on some gnarly CSS3 to specify that ingredient "kind" comes before the "name": .ingredient > .kind + .name. The whole example is:
    it "should read conversationally, with the ingredient kind before the name" do
response.should have_selector(".preparations") do |preparations|
preparations.
should have_selector(".ingredient > .kind + .name",
:content => 'flour')
end
end
Making the spec pass is a simple matter of re-arranging the Haml template.
(commit)

While I was in the view, I also took some time to arrange the brand (e.g. Nestle Tollhouse) in the ingredient listing.
(commit)

Moving back out to the Cucumber scenario (and adding the requisite ingredient preparations), I now have the entire scenario passing:
cstrom@jaynestown:~/repos/eee-code$ cucumber features/recipe_details.feature -n -s "Viewing a recipe with several ingredients"
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: Viewing a recipe with several ingredients
Given a recipe for Buttermilk Chocolate Chip Pancakes
When I view the recipe
Then I should see an ingredient of "1 cup all-purpose, unbleached flour"
And I should see an ingredient of "¼ teaspoons salt"
And I should see an ingredient of "chocolate chips (Nestle Tollhouse)"


1 scenario
5 steps passed
(commit)

Saturday, March 28, 2009

Inside with View Specs

‹prev | My Chain | next›

Cleanup for Cross-Project Consistency

Reading through the RSpec Beta book, I noticed that view specs are rendered without the initial slash (e.g. render("views/recipe.haml")). To prevent confusion when switching between projects, I modify the my Sinatra spec helper to work with or without the initial slash:
def render(template_path)
template = File.read("./#{template_path.sub(/^\//, '')}")
engine = Haml::Engine.new(template)
@response = engine.render(Object.new, assigns_for_template)
end
(commit)

So now, it's back to view spec'ing...

Specifying the Ingredients Display

We already have the recipe title showing, next up is displaying ingredients. On the recipe page, there should be a section listing the ingredients, with simple preparation instructions (e.g. diced, minced, 1 cup measured, etc). This can be expressed in RSpec as:
  it "should render ingredient names" do
render("views/recipe.haml")
response.should have_selector(".preparations") do |preparations|
prepartions.
should have_selector(".ingredient > .name", :content => 'egg')
end
end
Running this spec fails because the needed HTML document structure is not in place:
strom@jaynestown:~/repos/eee-code$ ruby ./spec/views/recipe.haml_spec.rb
.F

1)
'recipe.haml should render ingredient names' FAILED
expected following output to contain a <.preparations/> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><h1>
Recipe Title
</h1></body></html>
./spec/views/recipe.haml_spec.rb:27:
./spec/views/recipe.haml_spec.rb:3:

Finished in 0.010398 seconds

2 examples, 1 failure
Implementing that document structure in Haml is pretty darn easy:
%h1
= @recipe['title']

%ul.preparations
%li.ingredient
%span.name
egg
Running the spec now passes:
cstrom@jaynestown:~/repos/eee-code$ ruby ./spec/views/recipe.haml_spec.rb
..

Finished in 0.01163 seconds

2 examples, 0 failures
A template that only includes a single ingredient of "egg" is not going to be all that useful. To get the ingredient name into the HTML output, we need to build up the @recipe data structure in the example. The ingredient preparation data structures in our legacy system are somewhat complex. For example, 10 ounces of frozen spinach defrosted in the microwave is represented as:
       {
"quantity": 10,
"ingredient": {
"name": "spinach",
"kind": "frozen"
},
"unit": "ounces",
"description": "cooked in microwave"
}
That level of complexity is not needed to specify that an ingredient name exists. Instead use an egg preparation of:
  @recipe['preparations'] =
[ 'quantity' => 1, 'ingredient' => { 'name' => 'egg' } ]
Updating the Haml template to display a recipe's ingredients is easy:
%h1
= @recipe['title']

%ul.preparations
- @recipe['preparations'].each do |preparation|
%li.ingredient
%span.name
= preparation['ingredient']['name']
(commit)

Uncover a Bug? Don't Fix It... Write a Spec!

Running the whole spec, however, causes a failure in the first example!
cstrom@jaynestown:~/repos/eee-code$ ruby ./spec/views/recipe.haml_spec.rb
F.

1)
NoMethodError in 'recipe.haml should display the recipe's title'
undefined method `each' for nil:NilClass
(haml):5:in `render'
/home/cstrom/.gem/ruby/1.8/gems/haml-2.0.9/lib/haml/engine.rb:149:in `render'
/home/cstrom/.gem/ruby/1.8/gems/haml-2.0.9/lib/haml/engine.rb:149:in `instance_eval'
/home/cstrom/.gem/ruby/1.8/gems/haml-2.0.9/lib/haml/engine.rb:149:in `render'
/home/cstrom/repos/eee-code/spec/spec_helper.rb:20:in `render'
./spec/views/recipe.haml_spec.rb:11:
./spec/views/recipe.haml_spec.rb:3:

Finished in 0.011262 seconds

2 examples, 1 failure
Ah, there are no preparation instructions in the first example. When a recipe includes no preparation / ingredients, nothing should be displayed:
  context "no ingredient preparations" do
before(:each) do
@recipe[:preparations] = nil
end

it "should not render an ingredient preparations" do
render("views/recipe.haml")
response.should_not have_selector(".preparations")
end
end
I like the use of context here. In describing the example above, I used the word "When", which suggests a specific context in which the spec runs. It also aids in readability. Even in an age in which monitors are 1600 pixels wide, the less horizontal scanning needed, the easier it is to read the core concept of the code / spec.

I can make this pass with a conditional in the Haml template:
%h1
= @recipe['title']

- if @recipe['preparations']
%ul.preparations
- @recipe['preparations'].each do |preparation|
%li.ingredient
%span.name
= preparation['ingredient']['name']
Making this spec page also resolves the problem in the original spec:
cstrom@jaynestown:~/repos/eee-code$ ruby ./spec/views/recipe.haml_spec.rb -cfs 

recipe.haml
- should display the recipe's title
- should render ingredient names

recipe.haml a recipe with no ingredient preparations
- should not render an ingredient preparations

Finished in 0.015988 seconds

3 examples, 0 failures
It would have been a mistake to attempt to fix the first spec failure directly by adding the conditional to the Haml template. The first spec was meant to test something very specific, if very simple. It happened to uncover a boundary condition. Fixing the boundary condition issue in that spec would have left the boundary condition uncovered. It also would not have given me the opportunity to codify my thinking in resolving the issue.

As it is, I am in a much better place now. I still have my original, simple spec which may yet uncover other boundary condition defects. I also have a specification of how I handle this specific boundary condition.
(commit)