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.
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.
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.
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.
Using nested #context
s 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.
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.
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.