9. Testing Your Controllers

9.1. What Is It?

The goal of functional testing is to test your controllers. When you get into the realm of testing controllers, we're operating at a higher level than the model. At this level, we test for things such as:

  • was the web request successful?

  • were we redirected to the right page?

  • were we successfully authenticated?

  • was the correct object stored in the response template?

Just as there is a one-to-one ratio between unit tests and models, so there is between functional tests and controllers. For a controller named HomeController, you would have a test case named HomeControllerTest.

9.2. An Anatomy Lesson

So let's take a look at an example of a functional test.

require File.dirname(__FILE__) + '/../test_helper'

class HomeControllerTest < ActionController::TestCase
  # let's test our main index page
  def test_index
    get :index
    assert_response :success
  end
end

9.2.1. Making the moves

In the one test we have called test_index, we are simulating a request on the action called index and making sure the request was successful.

The get method kicks off the web request and populates the results into the response. It accepts 4 arguments.

  • The action of the controller you are requesting. It can be in the form of a string or a symbol. Cool people use symbols. ;)

  • An optional hash of request parameters to pass into the action (eg. query string parameters or post variables).

  • An optional hash of session variables to pass along with the request.

  • An optional hash of flash to stash your goulash.

Example: Calling the :show action, passing an id of 12 as the params and setting user_id of 5 in the session.

get(:show, {'id' => "12"}, {'user_id' => 5})

Another example: Calling the :view action, passing an id of 12 as the params, this time with no session, but with a flash message.

get(:view, {'id' => '12'}, nil, {'message' => 'booya!'})

9.2.2. Available at your disposal

For those of you familiar with HTTP protocol, you'll know that get is a type of request. There are 5 request types supported in Rails:

  • get

  • post

  • put

  • head

  • delete

All of request types are methods that you can use, however, you'll probably end up using the first two more ofter than the others.

9.3. The 4 Hashes of the Apocolypse

After the request has been made by using one of the 5 methods (get, post, etc…), you will have 4 Hash objects ready for use.

They are (starring in alphabetical order):

assigns

Any objects that are stored as instance variables in actions for use in views.

cookies

Any objects cookies that are set.

flash

Any objects living in the flash.

session

Any object living in session variables.

For example, let's say we have a MoviesController with an action called movie. The code for that action might look something like:

def movie
  @movie = Movie.find(params[:id])
  if @movie.nil?
    flash['message'] = "That movie has been burned."
    redirect_to :controller => 'error', :action => 'missing'
  end
end

Now, to test out if the proper movie is being set, we could have a series of tests that look like this:

# this test proves that fetching a movie works
def test_successfully_finding_a_movie
  get :movie, "id" => "1"
  assert_not_nil assigns["movie"]
  assert_equal 1, assigns["movie"].id
  assert flash.empty?
end

# and when we can't find a movie...
def test_movie_not_found
  get :movie, "id" => "666999"
  assert_nil assigns["movie"]
  assert flash.has_key?("message")
  assert assigns.empty?
end

As is the case with normal Hash objects, you can access the values by referencing the keys by string. You can also reference them by symbol name… except assigns. Check it out:

flash["gordon"]               flash[:gordon]
session["shmession"]          session[:shmession]
cookies["are_good_for_u"]     cookies[:are_good_for_u]

# Because you can't use assigns[:something] for historical reasons:
assigns["something"]          assigns(:something)

Keep an eye out for that. mmmm kay?

There are 3 assertions that deal with the overall response to a request. They are:

assert_template( expected_template, [msg] )

Ensures the expected template was responsible for rendering. For example:

assert_template "user/profile"

This code will fail unless the template located at app/views/user/profile.rhtml was rendered.

assert_response( type_or_code, [msg] )

Ensures the response type/status code is as expected. For example:

assert_response :success      # page rendered ok
assert_response :redirect     # we've been redirected
assert_response :missing      # not found
assert_response 505           # status code was 505

The possible options are:

  • :success (status code is 200)

  • :redirect (status code is within 300..399)

  • :missing (status code is 404)

  • :error (status code is within 500..599)

  • any number (to specifically reference a particular status code)

assert_redirected_to ( options={}, [msg] )

Ensures we've been redirected to a specific place within our application.

assert_redirected_to :controller => 'widget', :action => 'view', :id => 555

The assert_tag and assert_no_tag assertions are for analysing the html returned from a request.

9.5.1. assert_tag( options )

Ensures that a tag or text exists. There are a whole whack o' options you can use to discover what you are looking for. Some of the conditions are like XPATH in concept, but this is sexier. In fact, let's call it SEXPATH.

The following description is lifted verbatim from the rails assertion docs.

Asserts that there is a tag/node/element in the body of the response that meets all of the given conditions. The conditions parameter must be a hash of any of the following keys (all are optional):

  • :tag : the node type must match the corresponding value

  • :attributes : a hash. The node's attributes must match the corresponding values in the hash.

  • :parent : a hash. The node's parent must match the corresponding hash.

  • :child : a hash. At least one of the node's immediate children must meet the criteria described by the hash.

  • :ancestor : a hash. At least one of the node's ancestors must meet the criteria described by the hash.

  • :descendant : a hash. At least one of the node's descendants must meet the criteria described by the hash.

  • :children : a hash, for counting children of a node. Accepts the keys:

    • :count : either a number or a range which must equal (or include) the number of children that match.

    • :less_than : the number of matching children must be less than this number.

    • :greater_than : the number of matching children must be greater than this number.

    • :only : another hash consisting of the keys to use to match on the children, and only matching children will be counted.

    • :content : (text nodes only). The content of the node must match the given value.

Conditions are matched using the following algorithm:

  • if the condition is a string, it must be a substring of the value.

  • if the condition is a regexp, it must match the value.

  • if the condition is a number, the value must match number.to_s.

  • if the condition is true, the value must not be nil.

  • if the condition is false or nil, the value must be nil.

These examples are taken from the same docs too:

  # assert that there is a "span" tag
  assert_tag :tag => "span"

  # assert that there is a "span" inside of a "div"
  assert_tag :tag => "span", :parent => { :tag => "div" }

  # assert that there is a "span" somewhere inside a table
  assert_tag :tag => "span", :ancestor => { :tag => "table" }

  # assert that there is a "span" with at least one "em" child
  assert_tag :tag => "span", :child => { :tag => "em" }

  # assert that there is a "span" containing a (possibly nested)
  # "strong" tag.
  assert_tag :tag => "span", :descendant => { :tag => "strong" }

  # assert that there is a "span" containing between 2 and 4 "em" tags
  # as immediate children
  assert_tag :tag => "span",
             :children => { :count => 2..4, :only => { :tag => "em" } }

  # get funky: assert that there is a "div", with an "ul" ancestor
  # and an "li" parent (with "class" = "enum"), and containing a
  # "span" descendant that contains text matching /hello world/
  assert_tag :tag => "div",
             :ancestor => { :tag => "ul" },
             :parent => { :tag => "li",
                          :attributes => { :class => "enum" } },
             :descendant => { :tag => "span",
                              :child => /hello world/ }

9.5.2. assert_no_tag( options )

This is the exact opposite of assert_tag. It ensures that the tag does not exist.

9.6.1. assert_generates( expected_path, options, defaults={}, extras = {}, [msg] )

Ensures that the options map to the expected_path.

opts = {:controller => "movies", :action => "movie", :id => "69"}
assert_generates "movies/movie/69", opts

9.6.2. assert_recognizes( expected_options, path, extras={}, [msg] )

Ensures that when the path is chopped up into pieces, it is equal to expected_options. Essentially, the opposite of assert_generates.

opts = {:controller => "movies", :action => "movie", :id => "69"}
assert_recognizes opts, "/movies/movie/69

# also, let's say i had a line in my config/routes.rb
# that looked like:
#
#    map.connect (
#      'calendar/:year/:month',
#      :controller => 'content',
#      :action => 'calendar',
#      :year => nil,
#      :month => nil,
#      :requirements => {:year => /\d{4}/, :month => /\d{1,2}/}
#    }
#
# Then, this would work too:
opts = {
  :controller => 'content',
  :action => 'calendar',
  :year => '2005',
  :month => '5'
}
assert_recognizes opts, 'calendar/2005/5'

9.6.3. assert_routing( path, options, defaults={}, extras={}, [msg] )

Ensures that the path resolves into options, and the options, resolves into path. It's a two-way check to make sure your routing maps work as expected.

This assertion is simply a wrapper around assert_generates and assert_recognizes.

If you're going to test your routes, this assertion might be your best bet for robustness (yes, the overused buzzword of the 90's).

opts = {:controller => "movies", :action => "movie", :id => "69"}
assert_routing "movies/movie/69", opts

9.7. Testing File Uploads

So your web app supports file uploads eh? Here's what you can do to test your uploads.

This tip is brought to you by Chris Brinker, the letter R and the number 12.

Chris says, “In order to test a file being uploaded you have to mirror what cgi.rb is doing with a multipart post. Unfortunately what it does is quite long and complex, this code takes a file on your system, and turns it into what normally comes out of cgi.rb.”

Here are some helper methods based on Chris' work that you'll need to squirrel away either in a new unit, or cut ‘n' pasted right into your test. Any errors with this are my fault.

# get us an object that represents an uploaded file
def uploaded_file(path, content_type="application/octet-stream", filename=nil)
  filename ||= File.basename(path)
  t = Tempfile.new(filename)
  FileUtils.copy_file(path, t.path)
  (class << t; self; end;).class_eval do
    alias local_path path
    define_method(:original_filename) { filename }
    define_method(:content_type) { content_type }
  end
  return t
end

# a JPEG helper
def uploaded_jpeg(path, filename=nil)
  uploaded_file(path, 'image/jpeg', filename)
end

# a GIF helper
def uploaded_gif(path, filename=nil)
  uploaded_file(path, 'image/gif', filename)
end

And to use this code, you'd have a test that would looks something like this:

def test_a_file_upload
  assert_equal 0, GalleryImage.count
  heman = uploaded_jpeg("#{File.expand_path(RAILS_ROOT)}/text/fixtures/heman.jpg")
  post :imageupload, 'imagefile' => heman
  assert_redirected_to :controller => 'gallery', :action => 'view'
  assert_equal 1, GalleryImage.count
end