time to step up

Jared Carroll

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.