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)

2 comments:

  1. Good work on the continued documentation of your chain. You're more disciplined with it than I.

    Have you seen Elementor? It probably wouldn't make your view specs any fewer lines overall (it might later, when you add more), but you could write things like:

    @page.should have(1).preparation_ingredient.with_text('egg')

    and

    @page.should have(0).preparations

    I've only used it myself in one small project so far but I enjoy the assigning of meaningful names to the selectors and then using those names to make specs that read really well.

    ReplyDelete
  2. If I have see Elementor, it has long since passed from my conscious. Looks very, very nice. I will have to investigate it in more detail...

    Thanks for cluing me in!

    ReplyDelete