January 10, 2011

#38 RSpec - Views Part 2

We'll continue looking into view specing techniques. In addition, we'll look how to test helpers.

Including A Layout

In Post #4, we introduced a #title helper method. To this method, we would pass the title of our post and it would then be displayed in a <h1> as well as in the <title>. How can we test this? One way is to render the application layout in our post index tests. In order to do this, we specify the layout to be rendered in addition to the main view file.

In order to get the tests to pass, we need to write the helper method and modify the application layout (Code not shown yet. See next sections). Those two tests are now reaching outside of the post index specs and testing other things. Depending on your preference this may be what you want.

spec/views/posts/index.html.haml_spec
# index.html.haml_spec
describe "posts/index.html.haml" do
  ...
  context 'when an admin is not signed in' do
    ...
    context "and no posts are present" do
      before do
        assign :posts, []
        render :file => "posts/index.html.haml", :layout => "layouts/application.html.haml"
      end

      it "displays the title as an h1" do
        rendered.should have_selector 'h1',
          :content => 'Building This Site',
          :count => 1
      end

      it "displays the title in <title>" do
        rendered.should have_selector 'title',
          :content => 'aRailsDemo | Building This Site'
      end

Message Expectations

Let's take another approach and keep our index specs focused. In this case, we just want to make sure that the #title helper method is being used. For this we set a message expectation with #should_receive.

We can add various levels of detail with #should_receive. At minimun, we could just state what method should be called. However, we can also state what arguments the method should receive and how many times the method is called. In addition, if the result of that method call is important for the rest of the spec to run, we can state what the output will be.

Since we haven't written #title yet, we need to stub that method out at the start of our tests.

spec/views/posts/index.html.haml_spec.rb, app/views/posts/index.html.haml
# index.html.haml_spec.rb
describe "posts/index.html.haml" do
  ...
  before do
    view.stub(:title)
  end

  context 'when an admin is not signed in' do
    ...
    context "and no posts are present" do
      before do
        assign :posts, []
      end

      it "uses the #title helper method" do
#       view.should_receive(:title)
        view.should_receive(:title).with "Building This Site"
#       view.should_receive(:title).once.with "Building This Site"
#       view.should_receive(:title).exactly(1).times.with "Building This Site"
#       view.should_receive(:title).with("Building This Site").and_return('yee haw')

        render
      end
___________________________________________________

-# index.html.haml
- title "Building This Site"
-# ...

Helper Methods

We now turn our attention to the #title helper. To test helpers, RSpec provides us with #helper. This method "returns an instance of ActionView::Base with the helper being specified mixed in, along with any of the built-in rails helpers." In addition, it includes ApplicationHelper by default. If we have a method in another helper module that we need to access, then we can just include that module in our test file.
spec/helpers/layout_helper_spec.rb, app/helpers/layout_helper.rb
# layout_helper_spec.rb
require 'spec_helper'

# include MyAwsomeHelper

describe LayoutHelper do
  describe "#title" do
    it "sets content_for(:title)" do
      helper.title 1
      helper.content_for(:title).should == '1'
    end
  end
end
___________________________________________________

# layout_helper.rb
module LayoutHelper
  def title(page_title)
    content_for(:title) { page_title.to_s }
  end
end

Stubbing A Template

Let's look at the application layout now. This layout will render a sidebar partial. This partial doesn't exist yet, so we can use RSpec's #stub_template. This will accept a hash of the partial we're stubbing and the response of the stub.

We finish up adding the necessary <h1> and <title> content.

spec/views/layouts/application.html.haml_spec.rb, app/views/layouts/application.html.haml
#application.html.haml_spec.rb
...
describe "layouts/application.html.haml" do
  before do
    stub_template "shared/_side_bar.html.haml" => 'sidebar template'
  end
  
  it "renders the sidebar" do
    render
    rendered.should contain 'sidebar template'
  end
  ...
  context "using content_for(:title)" do
    let(:page_title) { "Building This Site" }

    before do
      view.content_for(:title) { page_title }
      render
    end

    it "sets <title>" do
      rendered.should have_selector :title,
        :content => "aRailsDemo | #{page_title}"
    end

    it "sets <h1>" do
      rendered.should have_selector :h1,
        :content => page_title
    end
___________________________________________________

# application.html.haml
!!!
%html
  %head
    %title
      = "aRailsDemo #{"| " + yield(:title) if content_for?(:title)}"
    ...
  %body
    - if content_for?(:title)
      %h1= yield(:title)

    = yield

  #sidebar
    = render 'shared/side_bar'

Mock Models And Forms

Next, we look at forms. We need to provide a model to Rail's form_for helper. The thing to keep in mind here is that mock_model by itself will act as an existing object. So the form that is generated will be an update form. If we want the model to act like a newly created object, we use #as_new_record.

These test, however, are testing some internals of Rails form_for method ('Is form_for rendering the form with the right action?') An alternative test is to just make sure that form_for was called.

Which ever way we choose, we can then test to make sure certain elements are in place. To get around the fact that our mock_model has no attributes, we have to use #as_null_object. Without this, when Rails asks "Hey mock object. What's your :title attribute?", RSpec will say "Hacka please. I ain't expectin' no :title message!" and raise an error. (This isn't a problem if we're using stub_model.)

spec/views/posts/_form.html.haml_spec.rb, app/views/posts/_form.html.haml
# _form.html.haml_spec.rb
...
describe "posts/_form.html.haml" do
  let(:existing_post) { mock_model('Post') }

  it "can render an 'update' form" do
    assign :post, existing_post
    render
    rendered.should have_selector 'form',
      :action => post_path(existing_post)
  end
################################################
  let(:new_post) { mock_model('Post').as_new_record }

  it "can render a 'create' form" do
    assign :post, new_post
    render
    rendered.should have_selector 'form',
      :action => posts_path
  end
################################################
  let(:new_post) { mock_model('Post').as_new_record.as_null_object }

  context "given a new post object" do
    before do
      assign :post, new_post
    end

    it "creates a form" do
      view.should_receive(:form_for).with new_post
      render
    end

    it "has a title text field" do
      render
      rendered.should have_selector :input,
        :type => 'text',
        :name => 'post[title]'
    end
  end
___________________________________________________

# _form.html.haml
= form_for @post do |f|
  = f.text_field :title

Working With Simple Form

Since we're using the simple_form gem for this site, we'll switch to that now. SimpleForm will look at our existing model to determine what kind of input fields to generate. Therefore we generate our Post and Section models now and switch to using stub_model instead of mock_model in our specs.* We also setup our Post and Section relationships.

(* Things get buggy when using mock_model and stub_model for the same model within or between spec files. This may be limited to just when Autotest and Spork are running. I switched to using only stub_model for all of the post specs.)

Gemfile, Terminal, app/models/post.rb and section.rb
# Gemfile
...
gem 'simple_form', '>= 1.3.0'
___________________________________________________

# Terminal
> bundle
> rails g model post title:string description:text sequence:integer status:string
> rails g model section heading:string body:text position:integer post_id:integer
> rake db:migrate && rake db:test:prepare
___________________________________________________

# post.rb
class Post < ActiveRecord::Base
  has_many :sections
  accepts_nested_attributes_for :sections
end
___________________________________________________

# section.rb
class Section < ActiveRecord::Base
  belongs_to :post
end

An RSpec Helper Module

RSpec allows us to include custom modules so that they are available in all of our tests. So let's create a module that will contain our model stub. This stub is a post that has a single section. It also has some default values which we can change if we need to.
spec/spec_helper.rb, spec/support/stubs.rb
# spec_helper.rb
...
  RSpec.configure do |config|
    ...
    config.include Stubs
  end
___________________________________________________

# stubs.rb
module Stubs
  def post_stub(stubs={})
    defaults = {
      :sequence => 1,
      :title => 'First Post',
      :description => 'Description1',
      :status => 'pending',
      :created_at => Time.utc(2000),
      :sections => [stub_model(Section)]
    }
    @post_stub ||= stub_model(Post, defaults.merge(stubs))
  end
end

Form Partials

Our post form will render a partial that contains the fields for the sections' fields. The trick here is that we need to pass in a form builder object to this partial. Fortunately, we can call form_for directly in our tests to create that builder object. SimpleForm can then use this builder object to create the fields that we are testing.
spec/sections/_form_fields.html.haml_spec.rb, app/views/sections/_form_fields.html.haml
# _form_fields.html.haml_spec.rb
...
describe "/sections/_form_fields.html.haml" do
  before do
    form_for(post_stub) { |f| @f = f }
    render :partial => 'sections/form_fields', :locals => { :builder => @f }
  end

  it "has a heading text field" do
    rendered.should have_selector :input,
      :id => 'post_sections_attributes_0_heading'
  end
___________________________________________________

-# _form.fields.html.haml
= builder.simple_fields_for :sections do |section_f|
  .section
    %h2 Section
    = section_f.input :heading

Conclusion

So are views worth testing especially in isolation from the controllers and models? Point's of view vary widely. In the end, it's just another tool for developing software. From the RSpec book: "The only way to really get a feel for the benefits of them [view tests] is to learn to write them well. And only once you really understand how they fit in the flow are you going to be able to make well-grounded decisions about if and when to use them." To each, his or her own.

(Addition view and helper tests for this post can be found at our Github repo.)

    Comments

(Please login to submit comments.)



View All Posts