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
# 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.)
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
... ... 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.
...
...
context "and no posts are present" do
before do
assign :posts, []
render
endView Matchers
# 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.
# 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).
# 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.)
> 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
(Please login to submit comments.)
Comments