Wolfmans Howlings

A programmers Blog about Ruby, Rails and a few other issues

Using RSpec to test HAML helpers

Posted by Jim Morris Sat, 14 Jul 2007 23:40:00 GMT

UPDATED for HAML 2.0 and RSpec 1.1.5 - Changed open to haml_tag, prefix helper. to all rspec calls...

The most recent release of HAML introduced a neat feature that allows you to use HAML-like syntax in your helpers to generate HTML HAML#haml_tag.

A question on the HAML news group asked how to test a helper that uses HAML#haml_tag (used to be open/puts) and thanks to Nathan on that list I finally got RSpec to do it. As shown below.

However a really good point was made that really in RSpec the way to test anything is to use mocks to mock any call to an outside method thus focusing the test on the specific module under test. Generally I agree with that philosophy. But this is way cool so I thought I'd do it anyway, and also as it is a new feature in HAML one may not want to simply trust HAML to generate the correct HTML.

So in my application_helper.rb I have a simple helper...

module ApplicationHelper

 ...

  def display_flash
    for name in [:notice, :warning, :error]
      if flash[name]
        haml_tag :div, flash[name], {:class => name.to_s}
      end
    end
    nil
  end

  ...

end

This is called in my views as...

- display_flash

Notice the - instead of =, this is because the open (and puts) write output directly to the HAML buffer, and so this routine should return nothing. (This is also a very simply case and does not show off the utility of the open/puts methods, I'll show one of those later on).

The RSpec helper test that tests this is as follows...

# File: spec/helpers/application_helper_spec.rb
require File.dirname(__FILE__) + '/../spec_helper'

describe ApplicationHelper do

  before :each do
    helper.extend Haml
    helper.extend Haml::Helpers 
    helper.send :init_haml_helpers
  end

  it "should display flash" do
    for name in [:notice, :warning, :error]
      flash[name]= "flash #{name.to_s} message"
      helper.capture_haml{
        helper.display_flash
      }.should =~ /<div class='#{name.to_s}'>\s*#{flash[name]}\s*<\/div>/
      flash[name]= nil
    end
  end

end

Excellent, a simple test for the HTML generated by my haml helper. NOTE the setup required in the before :each, this sets up the haml helpers in the helpers context

Why would I want to use HAML#open you ask?

Well it makes the helpers look so much tidier IMHO, take this example from my previous post on tag clouds, the re-factored helper now looks like this...

  # display a tag cloud for the given model
  def tag_cloud(model, title= nil)
    m= model.to_s.camelcase.constantize
    plural= model.to_s.capitalize.pluralize
    title ||= plural
    tags= m.tag_counts(:order => 'tags.name')
    return false if tags.empty?
    urlmeth= "tagged_#{model.to_s.pluralize}_path".to_sym
    haml_tag :div, {:class => "tagcloud"} do
      haml_tag :h3, title
      tags.each do |t|
        next if t.name == 'FAQ'
        haml_tag :span, {:style => "font-size:#{calc_size(t.count)}%"} do
          puts link_to(h(t.name), self.send(urlmeth, :tag => t.name))
        end
      end
    end
    return true
  end      

So much cleaner, plus I can return a boolean to indicate if there was anything output or not, which tells me if I need to output an <hr/> or not.

Posted in , ,  | Tags , , ,  | 8 comments | no trackbacks

Using RSpec to test HAML helpers

Posted by Jim Morris Sat, 14 Jul 2007 23:40:00 GMT

UPDATED for HAML 2.0 and RSpec 1.1.5 - Changed open to haml_tag, prefix helper. to all rspec calls...

The most recent release of HAML introduced a neat feature that allows you to use HAML-like syntax in your helpers to generate HTML HAML#haml_tag.

A question on the HAML news group asked how to test a helper that uses HAML#haml_tag (used to be open/puts) and thanks to Nathan on that list I finally got RSpec to do it. As shown below.

However a really good point was made that really in RSpec the way to test anything is to use mocks to mock any call to an outside method thus focusing the test on the specific module under test. Generally I agree with that philosophy. But this is way cool so I thought I'd do it anyway, and also as it is a new feature in HAML one may not want to simply trust HAML to generate the correct HTML.

So in my application_helper.rb I have a simple helper...

module ApplicationHelper

 ...

  def display_flash
    for name in [:notice, :warning, :error]
      if flash[name]
        haml_tag :div, flash[name], {:class => name.to_s}
      end
    end
    nil
  end

  ...

end

This is called in my views as...

- display_flash

Notice the - instead of =, this is because the open (and puts) write output directly to the HAML buffer, and so this routine should return nothing. (This is also a very simply case and does not show off the utility of the open/puts methods, I'll show one of those later on).

The RSpec helper test that tests this is as follows...

# File: spec/helpers/application_helper_spec.rb
require File.dirname(__FILE__) + '/../spec_helper'

describe ApplicationHelper do

  before :each do
    helper.extend Haml
    helper.extend Haml::Helpers 
    helper.send :init_haml_helpers
  end

  it "should display flash" do
    for name in [:notice, :warning, :error]
      flash[name]= "flash #{name.to_s} message"
      helper.capture_haml{
        helper.display_flash
      }.should =~ /<div class='#{name.to_s}'>\s*#{flash[name]}\s*<\/div>/
      flash[name]= nil
    end
  end

end

Excellent, a simple test for the HTML generated by my haml helper. NOTE the setup required in the before :each, this sets up the haml helpers in the helpers context

Why would I want to use HAML#open you ask?

Well it makes the helpers look so much tidier IMHO, take this example from my previous post on tag clouds, the re-factored helper now looks like this...

  # display a tag cloud for the given model
  def tag_cloud(model, title= nil)
    m= model.to_s.camelcase.constantize
    plural= model.to_s.capitalize.pluralize
    title ||= plural
    tags= m.tag_counts(:order => 'tags.name')
    return false if tags.empty?
    urlmeth= "tagged_#{model.to_s.pluralize}_path".to_sym
    haml_tag :div, {:class => "tagcloud"} do
      haml_tag :h3, title
      tags.each do |t|
        next if t.name == 'FAQ'
        haml_tag :span, {:style => "font-size:#{calc_size(t.count)}%"} do
          puts link_to(h(t.name), self.send(urlmeth, :tag => t.name))
        end
      end
    end
    return true
  end      

So much cleaner, plus I can return a boolean to indicate if there was anything output or not, which tells me if I need to output an <hr/> or not.

Posted in , ,  | Tags , , ,  | 8 comments | no trackbacks

RSpec testing views for escaped HTML

Posted by Jim Morris Sat, 07 Jul 2007 01:22:00 GMT

For my social networking site snowdogsr.us I decided to escape all user input that gets displayed. I know people like to trick out their profiles with HTML but I want to avoid the various hacks that it allows.

So thinking I had done a good job of using h everywhere I output user input fields, I decided to see if I could actually test this with RSpec view tests.

I recently switched to RSpec for my testing needs, its cool :)

One thing it does is isolate the various things for testing using built in mocking, and views can be entirely tested standalone without accessing a model or a controller.

So how would it do testing for escaped user input I wondered?

Very well actually.

An example is worth a thousand words, so here is my RSpec for my home page.

BTW I found about 4 places where embedded HTML in user input was bleeding through, so it was well worth the effort.

So this goes in spec/views/home/home_spec.rb...

  it "should escape all user input" do
    @place= mock_model(Place, :name => 'place name<b>', :location => 'place location<b>', :tag_list => "place taglist <b>", :rated? => false)
    @event= mock_model(Event, :name => 'event name<b>', :where => 'event where<b>', :tag_list => "event taglist <b>", :date_time => DateTime.now, :hosted_by => 'Event host<b>')
    @post= mock_model(Input, :input => 'input body <b>', :tag_list => "post taglist <b>", :updated_at => DateTime.now, :created_at => DateTime.now, :created_by => 'post created by person<b>', :rated? => false)
    @picture= mock_model(Picture, :public_filename => "filename<b>.png")
    @pictures= [@picture]
    @pet= mock_model(Pet, :name => 'pet name<b>', :owned_by => "pet owner <b>", :breed => 'breed <b>', :description => "pet description <b>", :neutered => true, :gender => 'M<b>', :pictures => @pictures, :owned_by? => false)

    @posts= [@post]
    @events= [@event]
    @places= [@place]
    @top_places= [@place]
    @new_pets= [@pet]

    @comment= mock_model(Comment)
    @comment.stub!(:user).and_return(@user)
    @comment.stub!(:created_at).and_return(DateTime.now)
    @comment.stub!(:comment).and_return('comment body <b>')
    @comments= [@comment]
    @post.should_receive(:comments).and_return(@comments)

    @new_stuff= []
    @new_stuff << {:list => @posts, :title => 'Posts', :link => '#'}
    @new_stuff << {:list => @events, :title => 'Events', :link => '#'}
    @new_stuff << {:list => @places, :title => 'Places', :link => '#'}

    @top= []
    @top << {:list => @top_places, :title => 'Hot Places', :link => '#'}

    assigns[:new_stuff] = @new_stuff
    assigns[:top] = @top
    assigns[:new_pets] = @new_pets

    render "/home/logged_in"   
    #puts excerpt(response.body, "<b>")
    response.should_not have_text(/<b>/)
  end

Its quite complex as the home page renders a lot of summaries of the various lists I have.

First I mock the models that are called, and stub out the calls that are made to them. I force them all to return an embedded <b> which I don't use anyway, and with the new CSS oriented web styles shouldn't be used in HTML anyway.

Then I just test that <b> does not appear anywhere. If I have correctly used h to escape all the inputs then it should be rendered as &lt;b&gt; instead.

The response.should_not have_text(/<b>/) should do that test.

One cool thing is the mocking will tell you if any new inputs (ie calls to model attributes) have been added, or if you have forgotten any. So this should keep you honest in the future if you add new attributes that need escaping.

The

assigns[:new_stuff] = @new_stuff
assigns[:top] = @top
assigns[:new_pets] = @new_pets

Sets the assigns to the variables that my view uses,simulating what the controller would pass in.

The mock_model calls at the top also use a shortcut to define all the attributes that get called, and what they return. You can also explicitly do this...

@post.should_receive(:comments).and_return(@comments)

If you read the RSpec docs you can see that you can also test for parameters passed in, how many times it is called and various other nice things.

I added this snippet taken from the rails helpers to aid in finding any errant HTML that bleads through. (I'm not sure how to call it from the RSpec so I just copied the code into a private method).


 private

  def excerpt(text, phrase, radius = 100, excerpt_string = "...")
    if text.nil? || phrase.nil? then return end
    phrase = Regexp.escape(phrase)

    if found_pos = text.chars =~ /(#{phrase})/i
      start_pos = [ found_pos - radius, 0 ].max
      end_pos   = [ found_pos + phrase.chars.length + radius, text.chars.length ].min

      prefix  = start_pos > 0 ? excerpt_string : ""
      postfix = end_pos < text.chars.length ? excerpt_string : ""

      prefix + text.chars[start_pos..end_pos].strip + postfix
    else
      nil
    end
  end

and you can see the call that shows me where the errant <b> is...

puts excerpt(response.body, "<b>")

I also have some setup code that handles the login and log out mocking, but I'll leave that for the end user to sort out ;)

So I think this will make sure that now and in the future this particular view will not bleed user input HTML.

Once I did the complex one above the rest of the views were much easier and quicker to implement. Here is an example of a really simple one...

  it "should escape all user input" do
    @person= mock_model(Person, :name => 'person name <b>', :first_name => 'person first name <b>', :last_name => 'person last name <b>', :alias => 'person alias <b>', :show_gender => 'Male', :about_me => 'about <b>', :updated_at => DateTime.now, :created_at => DateTime.now, :pets => [])    

    assigns[:person] = @person

    render "/people/show"   

    response.should_not have_text(/<b>/)
  end

Couldn't be much simpler, but I found one place where I was not escaping the HTML!

Posted in ,  | Tags , ,  | 4 comments | no trackbacks

Older posts: 1 2 3