Friday, March 27, 2009

RSpec with Sinatra & HAML

‹prev | My Chain | next›

Last night, I was able to get HAML working together with Sinatra and CouchDB. Unfortunately, I got stuck trying to spec HAML views independently of the Sinatra application. I have grown accustomed to the cushy life afforded by rspec-on-rails and need to be able to do similar things with HAML.

The assigns[] and render methods are nowhere to be found—so how do I spec the views? The answer is to instantiate a Haml::Engine object and manually invoke its render method:
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper' )
require 'haml'

describe "recipe.haml" do
before(:each) do
@title = "Recipe Title"
@recipe = { 'title' => @title }

template = File.read("./views/recipe.haml")
@engine = Haml::Engine.new(template)
end

it "should display the recipe's title" do
response = @engine.render(Object.new, :@recipe => @recipe)
response.should have_selector("h1", :content => @title)
end
end
The call to render requires two arguments as documented in the Haml API. The first is the scope in which the the template is evaluated. It is useful if you want to bind an object whose methods can be evaluated by the HAML template. Since I have no need for this, I simply pass in a new, top-level Object instance (which is the default).

The second argument to render is a hash assignment of local variables. In this case, I want the template to see an instance variable @recipe and assign it the value of the @recipe variable defined in the before(:each). To do this, pass in a single-record hash with a key of :@recipe and value of the before(:each)'s @recipe instance variable.

With that in place, the spec passes:
cstrom@jaynestown:~/repos/eee-code$ ruby ./spec/views/recipe.haml_spec.rb 
.

Finished in 0.008689 seconds

1 example, 0 failures
(commit, commit)

It is working, but I would much prefer to keep the Haml::Engine overhead out of my view specs. In other words, I would like to have these looking more like normal view specs:
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper' )

describe "recipe.haml" do
before(:each) do
@title = "Recipe Title"
@recipe = { 'title' => @title }

assigns[:recipe] = @recipe
end

it "should display the recipe's title" do
render("/views/recipe.haml")
response.should have_selector("h1", :content => @title)
end
end
That turns out to be not nearly as difficult as I expected it would be. Here is the updated spec_helper.rb that allows the above spec to pass (comments inline):
ENV['RACK_ENV'] = 'test'

require 'eee'
require 'spec'
require 'spec/interop/test'
require 'sinatra/test'

require 'webrat'
require 'haml'

Spec::Runner.configure do |config|
config.include Webrat::Matchers, :type => :views
end

# Renders the supplied template with Haml::Engine and assigns the
# @response instance variable
def render(template)
template = File.read(".#{template}")
engine = Haml::Engine.new(template)
@response = engine.render(Object.new, assigns_for_template)
end

# Convenience method to access the @response instance variable set in
# the render call
def response
@response
end

# Sets the local variables that will be accessible in the HAML
# template
def assigns
@assigns ||= { }
end

# Prepends the assigns keywords with an "@" so that they will be
# instance variables when the template is rendered.
def assigns_for_template
assigns.inject({}) do |memo, kv|
memo["@#{kv[0].to_s}".to_sym] = kv[1]
memo
end
end
I am fairly pleased with my poor-man's rspec-for-haml-views implementation—mostly in that it actually works. It is not as robust as it would need to be for a gem release—when you neglect to set the proper assigns, you tend to get errors along the lines of:
undefined method `[]' for nil:NilClass
(haml):2:in `render'
Still, it is good enough for now. More importantly it gave me a chance to explore how these things are typically implemented—good knowledge to have in my toolbelt.
(commit)

1 comment:

  1. Thanks for this, it was very helpful. I'm just starting with tests on rails and I wanted to write some tests for Sinatra helpers and HAML and I found your post very useful!

    ReplyDelete