A Look at Specifying Views in RSpec

Jared Carroll ·

Like most developers I previously wrote off RSpec’s view specs entirely and instead decided to exclusively use Cucumber to test drive my views. But after reading the chapter on view specs in the recent RSpec book I decided to give them another try on a recent app.

The app was a traditional CMS with an admin and non-admin section, with the admin being mostly simple CRUD controllers for creating different types of pages. I modified my usual workflow of Cucumber feature then view implementation to instead be Cucumber feature, view spec, then view implementation. Here’s my postmortem on the whole adventure.

Specifying CRUD Forms

I want to first take a look at creating pages in the CMS. This was traditional CRUD and I feel it didn’t benefit from being covered by both Cucumber features and view specs.

The following is one of the scenarios from the CRUD feature for a “basic page” in the CMS (an example of a “basic page” would be an about page or a legal page).

  Scenario: Create a basic page
    Given I am signed in as an admin
    When I go to the admin dashboard
      And I follow "New Basic Page"
      And I fill in "Title" with "About"
      And I fill in "Body" with "About..."
      And I press "Save"
    Then I should see "Page created"
      And the "Title" field should contain "About"
      And the "Body" field should contain "About..."

Running this results in the following error (assume the routes, sign in and admin dashboard are already built and tested):

(::) failed steps (::)

cannot fill in, no text field, text area or password field with id, name, or label 'Title' found (Capybara::ElementNotFound)

This is a good error message because it tells you what you need to do next. We’ve all written these types of simple forms so many times that we usually skip the view spec and go straight to the implementation.

Instead let’s write a view spec first.

require 'spec_helper'

describe 'admin/pages/new.html.haml' do

  before do
    page = Factory.build :page
    assign :page, page

    render
  end

  it 'displays a form to create a page' do
    rendered.should have_selector('form',
                                  :id => 'new_page',
                                  :method => 'post',
                                  :action => '/admin/pages') do |form|
      form.should have_selector('input',
                                :type => 'submit')
    end
  end

  it 'displays a field to enter a title' do
    rendered.should have_selector('form',
                                  :id => 'new_page') do |form|
      form.should have_selector('label',
                                :for => 'page_title')
      form.should have_selector('input',
                                :type => 'text',
                                :name => 'page[title]')
    end
  end

  it 'displays a text area to enter a body' do
    rendered.should have_selector('form',
                                  :id => 'new_page') do |form|
      form.should have_selector('label',
                                :for => 'page_body')
      form.should have_selector('textarea',
                                :name => 'page[body]')
    end
  end

end

Does this spec really gain us anything though? I mean we already have a Cucumber feature that exercises this same form. One thing is certain is that this spec is certainly verbose. I followed this approach and the code to test ratio quickly got uncomfortably high. I managed to DRY a lot of it up using RSpec’s shared examples but it felt like over-specifying and just wrong, so perhaps it was a bit too much testing.

Writing these view specs did allow me to take very small steps making it easy to determine why a test is failing. But for obvious Cucumber failures view specs were hard to justify, although I could see them being helpful when the Cucumber failure isn’t that clear.

The RSpec book mentioned that for basic CRUD views view specs might not be that valuable. For straightfoward forms like the example above I agree and feel its best to not write view specs for them, instead rely on higher level Cucumber features for coverage.

Specifying Optional Information

As the app evolved image support was added to pages. Pages could now have images which would be displayed when editing a page. If the page didn’t have an image then no image would be displayed. This is just view logic to conditionally show a page’s optional image. In this case I felt view specs really felt appropriate because it felt more like an edge case than a feature. However, let’s first look at this in our Cucumber feature by adding some additional scenarios.

  Scenario: Editing a page with an image
    Given I am signed in as an admin
      And the following page exists:
        | title | image_file_name |
        | About | image.png       |
    When I go to edit the page titled "About"
    Then I should a thumbnail of "image.png"

  Scenario: Editing a page with NO image
    Given I am signed in as an admin
      And the following page exists:
        | title |
        | About |
    When I go to edit the page titled "About"
    Then I should not see a thumbnail image

To me these two specifications are at the wrong level. Although they specify the desired functionality it just seems like too much detail for a Cucumber feature. Instead I think this is better specified one layer down in a view spec.

require 'spec_helper'

describe 'admin/pages/edit.html.haml' do

  context 'given a page with an image' do
    before do
      @page = Factory(:page,
                      :image_file_name => 'image.png')
      assign :page, @page

      render
    end

    it 'displays a thumbnail of the image' do
      rendered.should have_selector('img.featured_image',
                                    :src => @page.image.url(:thumbnail))
    end
  end

  context 'given a page with NO image' do
    before do
      page = Factory(:page,
                     :image_file_name => nil)
      assign :page, page

      render
    end

    it 'does NOT display a thumbnail of its image' do
      rendered.should_not have_selector('img.featured_image')
    end
  end

end

By following this approach we keep the number of scenarios per feature down and instead just have general, broadstroke scenarios in our features. In my opinion a large number of scenarios in a feature feels like overspecifying at too high a level.

I think this is where view specs work great. Here we have some simple logic in our view that would just be overkill to specify at the Cucumber level but fits perfect one layer down in a view spec.

Implicit Assign

In view specs you use #assign to setup instance variables for the view you’re specifying. If your view spec has an instance variable that is the same name as an instance variable used in the view you don’t need to use #assign.

For example, given the following view:

app/views/pages/basic.html.haml

%h1#title= @page.title.upcase
#content= @page.body

The following spec doesn’t use #assign but will still pass.

require 'spec_helper'

describe 'pages/basic.html.haml' do

  before do
    @page = Factory :page

    render
  end

  it 'displays its title uppercased' do
    rendered.should have_selector('#title',
                                  :content => @page.title.upcase)
  end

  it 'displays its body' do
    rendered.should have_selector('#content',
                                  :content => @page.body)
  end

end

As “convenient” as this was, I still explicitly used #assign to make the specs more obvious and self documenting.

Nested #contexts

Using nested #contexts in view specs will concatenate the rendered HTML in each #context.

For example, given the following view:

app/views/pages/category.html.haml

- if @page.image.present?
  = image_tag @page.image.url(:thumbnail), :class => 'category'

%h1#title= @page.title

And the following spec that’s using a nested #context.

require 'spec_helper'

describe 'pages/category.html.haml' do

  context 'given a category' do
    before do
      @category = Factory :category
      assign :page, @category

      render
    end

    it 'displays its title' do
      rendered.should have_selector('#title',
                                    :content => @category.title)
    end

    context 'with an image' do
      before do
        image = Factory :image
        @category.update_attribute :image, image
        assign :page, @category

        render
      end

      it 'displays its image' do
        rendered.should have_selector('img.category',
                                      :src => @category.image.url(:thumbnail))
      end
    end
  end

end

In the nested “with an image” #context #rendered will return the following markup:

<h1 id='title'>title</h1>
<img alt="Attachment_file_name" class="category" src="http://s3.amazonaws.com/app-test/images/2015/thumbnail/attachment_file_name.png" />
<h1 id='title'>title</h1>

This output is from both #render messages. In most specs this behavior might not be an issue but it still caught me off guard enough to avoid it out of fear of obscure bugs.

Specifying #content_for

When your app has view specific content that should be displayed in a layout you use #yield in your layout and then put your view specific content in a #content_for block in your view. In view specs, markup rendered in a #content_for block is not returned by #rendered. However, it is accessible off of the view object that’s also available in view specs.

The following is specifying some view specific content for the sidebar section of the view’s layout.

require 'spec_helper'

describe 'pages/category.html.haml' do

  context 'given a category with a quotation' do
    before do
      @quotation = Factory :quotation
      category = Factory(:category,
                         :quotation => @quotation)
      assign :page, category

      render
    end

    it 'displays its quotation in the sidebar' do
      view.content_for(:sidebar).should have_selector('div.quote') do |div|
        div.should have_selector('blockquote',
                                 :content => @quotation.quote)
      end
    end
  end

end

Here we used the view object’s #content_for method passing it the name of the section of content that we’re specifying.

Conclusion

After a month of writing all views test first I feel their sweet spot is beyond simple CRUD forms, which Cucumber already provides enough coverage, and lies in specifying optional information. Using view specs to take smaller steps towards the implementation was fun and helped with debugging but at times it did get rather tedious. In the end view specs are just another tool to use in your testing strategy but first give them a try to see if they work for you.