January 08, 2011

#37 RSpec - Views Part 1

There is some debate about how detailed view tests should be and whether they're needed at all, but we should know how to test them regardless. In this post, we will look at developing our views with RSpec. The focus is to learn about RSpec's framework. What parts of the views to test and how to create tests that aren't fragile is another topic. (BTW: I highly recommend the RSpec book .)

Starting Afresh

To make things simpler, we're going to create a new Rails app. (The '-T' option will skip Test::Unit files, while the '-J' option will skip Prototype files. Refer to Post #35 for Spork bootstrapping.)
Terminal, Gemfile
# Terminal
> rails new testing_this_site -T -J
_____________________________________________________

# Gemfile
source 'http://rubygems.org'

gem 'rails', '3.0.3'
gem 'sqlite3-ruby', :require => 'sqlite3'
gem 'haml-rails', '>= 0.3.4'

group :development, :test do
  gem 'rspec-rails', '>= 2.4.1'
  gem 'webrat', '>= 0.7.3'
  gem 'spork', '>= 0.9.0.rc2'
end
_____________________________________________________

# Terminal
> bundle
> rake db:migrate && rake db:test:prepare
> rails g rspec:install
> spork --bootstrap

Mock Models

(To keep this post more condensed, I will not be presenting this in the usual BDD flow i.e. small red-green-refactor cycles.)

Let's start with the index page for posts. In the describe block, we tell RSpec where the view file of interest is located. We know we're going to be working with the Post model, but that isn't written yet. In this case we can use RSpec's mock_model.

Using mock_model, we specify the model's name and the attributes that it has. Since we're dealing with the index page, we define two mock_models. Using RSpec's let(), we can access these mocks by calling them like a method. (' let(:post1) ' does actually define a method #post1. In addition, it adds memoization to that method.)

(If we already had a Post model, then we could use stub_model which would make an instance of our ActiveRecord model. There are other differences between these two types of test doubles. See the docs for more information.)

spec/views/posts/index.html.haml_spec.rb
require "spec_helper"

describe "posts/index.html.haml" do
  let(:post1) do
    mock_model('Post',
      :sequence => 1,
      :title => 'First Post',
      :description => 'Description1',
      :created_at => Time.utc(2000))
  end

  let(:post2) do
    mock_model('Post',
      :sequence => 2,
      :title => 'Second Post',
      :description => 'Description2',
      :created_at => Time.utc(2001))
  end

Stubbing Helper Methods

We know that our view will change depending on whether an admin is logged in or not. Since we don't have this #admin? helper method yet, we can just stub that method on the view object. (Note: before() is the same as before(:each).)
spec/views/posts/index.html.haml_spec.rb
...
  ...
  context 'when an admin is not signed in' do
    before do
      view.stub(:admin?) { false }
    end

Instance Variables and Rendering The View

For our first set of specs, we consider the case where there are no posts. By convention, we'll have assigned @posts in our controller to be used in the view. Since we're testing the view in isolation of the controller, we manually assign the value of @posts for our tests.

To have our view rendered, we'll use the #render method. This method delegates to ActionView::Base#render and if passed no arguments, will use the view file that we are working on.

spec/views/posts/index.html.haml_spec.rb
...
  ...
    context "and no posts are present" do
      before do
        assign :posts, []
        render
      end

View Matchers

Now we write some actual tests. The first says that there will be a a single h1 tag with our title. After that we make sure that post related divs are not displayed. These tests make use of the #have_selector matcher which is provided by Webrat. We can additionally specify attributes on those selector elements. To get things to pass, we create our index.html.haml file and add our h1 tag.
spec/views/posts/index.html.haml_spec.rb, app/views/posts/index.html.haml
# index.html.haml_spec.rb
...
  ...
    ...
      it "displays the title as an h1" do
        rendered.should have_selector 'h1',
          :content => 'Building This Site',
          :count => 1
      end

      %W(postShow sequence title date).each do |klass|
        it "does not display div with class '#{klass}'" do
          rendered.should_not have_selector 'div', :class => klass
        end
      end
_____________________________________________________

-# index.html.haml
%h1 Building This Site

Custom Method Scope

For the case where two posts are displayed, there should be nested divs that have classes corresponding to the displayed attributes and contain the corresponding attribute values.

In refactoring, we created a #post1_displayed_attributes method that contained some of the attribute-value pairs that we are looking for. Notice that this is a regular method definition outside the RSpec example group. We have to put it here because the method is being called outside of an "it" or "before" block. If we were calling it within an "it"/"before" block, we could define it with let() or def within or before the context that is calling it.

We update our index file and en.yml (not shown) to make things pass.

spec/views/posts/index.html.haml_spec.rb, app/views/posts/index.html.haml
# index.html.haml_spec.rb
...
def post1_displayed_attributes
  {
    :sequence => 1,
    :title => 'First Post',
    :date => "January 01, 2000"
  }
end

describe "posts/index.html.haml" do
  ...
    ...
    context "and two posts are present" do
      before do
        assign :posts, [post1, post2]
        render
      end

      post1_displayed_attributes.each_pair do |attribute, value|
        it "displays div.#{attribute} with '#{value}'" do
          rendered.should have_selector 'div > div',
            :class => attribute.to_s,
            :content => value.to_s
        end
      end
_____________________________________________________

-# index.html.haml
%h1 Building This Site

- @posts.each do |post|
  .postShow
    .sequence= post.sequence
    .title= post.title
    .date= l post.created_at, :format => :date

Nested Selectors

Let's move on to the case where an admin is logged in. We want to see that a destroy link is present. This link will be nested in a div.admin. For this, we can pass a block to #have_selector. Within that block, we have access to just the matched selector.

Recall that the destroy link is implemented via JavaScript and depends on the link having a 'data-method' of 'delete'. Also, we are making use of our route helper methods, so we have defined the post routes in route.rb (not shown).

spec/views/posts/index.html.haml_spec.rb, app/views/posts/index.html.haml
# index.html.haml_spec.rb
...
  context "when an admin is signed in and there is one post" do
    before do
      view.stub(:admin?) { true }
      assign :posts, [post1]
      render
    end

    it "has a destroy link for that post inside .admin" do
      rendered.should have_selector('.postShow > .admin') do |admin_div|
        admin_div.should have_selector 'a',
          :href => post_url(post1.sequence),
          :"data-method" => "delete",
          :content => 'Destroy'
      end
    end
  end

_____________________________________________________

-# index.html.haml
-#...
- @posts.each do |post|
  .postShow
    -#...
    -if admin?
      .admin
        = link_to 'Destroy', post_url(post.sequence), :confirm => 'Are you sure?', :method => :delete

Adding A Few More Specs

The rest of the specs and the resulting view file can be found here. We'll continue looking more into view testing techniques in the next post.

(Note: There is a bug in rspec-core v2.4.0 that prevents using the documentation format when we have --drb specified. This is fixed in master.)

Terminal
> rspec spec --format documentation
posts/index.html.haml
  when an admin is not signed in
    and no posts are present
      displays the title as an h1
      does not display div with class 'postShow'
      does not display div with class 'sequence'
      does not display div with class 'title'
      does not display div with class 'date'
    and two posts are present
      displays div.title with 'First Post'
      displays div.sequence with '1'
      displays div.date with 'January 01, 2000'
      displays div.title with 'Second Post'
      displays div.sequence with '2'
      displays div.date with 'January 01, 2001'
      does not have a delete link
      does not have a new link
  when an admin is signed in and there is one post
    has an edit link for that post inside .admin
    has a destroy link for that post inside .admin
    has a new link

    Comments

(Please login to submit comments.)



View All Posts