I’ve ignored testing my views in Rails for far too long. So I’ve started to try some stuff out in my existing functional tests.
Using assert_select
This is a really powerful assertion added to Test::Unit::TestCase
that allows
you to use CSS
selectors when testing views and is the basis for all my view
testing.
Say we have locations
. A Location
object has only 2 fields:
- name - varchar
- address - text
We’ll have a LocationsController
to handle all our Location
CRUD.
class LocationsController < ApplicationController
end
Let’s start out with the C in CRUD, Create.
Here’s our functional test method for our LocationsController#new
action.
def test_should_create_a_new_location_object_on_GET_to_new
get :new
assert_kind_of Location, assigns(:location)
assert_response :success
assert_template 'new'
assert_select 'form[action=?]', locations_path, true,
'no form to locations#create was found'
assert_select 'form input', 2,
'2 input fields were not found within a form' do
assert_select '[type=text][name=?]', 'location[name]', true,
"no text field for a location's name was found within a form"
assert_select '[type=submit][value=Create]', true,
'no submit button was found within a form'
end
assert_select 'form textarea[name=?]', 'location[address]', true,
"no textarea for a location's address was found within a form"
assert_recognizes({ :controller => 'locations',
:action => 'new' },
:path => 'locations/new',
:method => :get)
end
This is a pretty comprehensive test.
- It does a GET to
locations#new
first and asserts that the correct instance variables were setup by the action - It then verifies the response was a success (200)
- It then verifies that the
new
template was rendered - It then moves on to some view testing
Now when it comes to view testing I’m going to take the approach of just making
sure the important stuff is in the view and not test for a
character-by-character exact match. When it comes to a new
action the most
important thing is going to be a HTML form element.
Let’s take a look at each of those assert_select
calls in detail.
assert_select 'form[action=?]', locations_path, true,
'no form to locations#create was found'
That one says there has to be at least 1 form whos action attribute is equal to
the url generated from the named route #locations_path
. The third boolean
parameter is required in order to output a nice helpful error message, a true
value means at least one element has to be found. The error message is key when
it comes to assert_select
because its default one is pretty cryptic (try it
out yourself).
assert_select 'form input', 2,
'2 input fields were not found within a form' do
assert_select '[type=text][name=?]', 'location[name]', true,
"no text field for a location's name was found within a form"
assert_select '[type=submit][value=Create]', true,
'no submit button was found within a form'
end
The next #assert_select
says there has to be at least 1 form on the page that
has 2 input fields. If that is satisfied it then yields each of those input
elements to its given block. The 2 #assert_select
calls inside the given
block are relative to the yielded input
element. The first one says one of
these input
elements I get has to have a type attribute of text and a name
attribute of location[name]
. The second one says one of these input
elements I get has to have a type attribute of submit and a value attribute of
‘Create’.
Error messages are used for all 3 assertions to improve debugging.
assert_select 'form textarea[name=?]', 'location[address]', true,
"no textarea for a location's address was found within a form"
The final #assert_select
call says there has to be at least 1 textarea element
whos name is location[address]
within a form element.
This #assert_select
could not be in previous #assert_select
‘s block because
a text area in HTML is a separate
element and NOT an 'input’ element with a type attribute value of ‘textarea’.
After all the #assert_select
call an #assert_recognizes
is used to verify
routing to LocationsController#new
.
assert_recognizes({ :controller => 'locations',
:action => 'new' },
:pat h => 'locations/new', :method => :get)
And here’s the normal looking controller.
class LocationsController < ApplicationController
def new
@location = Location.new
end
end
And the view
In app/views/locations/new.rhtml
:
<% form_for :location, :url => locations_path do |form| -%>
<%= form.text_field :name %>
<%= form.text_area :address %>
<%= submit_tag 'Create' %>
<% end -%>
Now looking at my functional test method, I have 6 lines related to view testing. You can probably argue that a form in a view is part of the functional requirements of the application because its absolutely needed by an action in some controller and therefore should be tested. Now this view is going to be ultimately rendered in a layout that probably includes a header, navigation menu, footer, etc. but I’m not going to test that in this functional test.
I have played around a little with ZenTest’s Test::Rails::ViewTestCase
but it
took a while to setup, required a couple of gems, had too many naming
conventions and was poorly documented when it comes to getting your feet wet
with it. So for the time being I’m going to stick with what comes with Rails.