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'
# grab our HomeController because we're going to test it
require 'home_controller'
# Raise errors beyond the default web-based presentation
class HomeController; def rescue_action(e) raise e end; end
class HomeControllerTest < Test::Unit::TestCase
def setup
@controller = HomeController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
# let's test our main index page
def test_index
get :index
assert_response :success
end
end
The big three
In the setup method, we create 3 objects:
- One of your controllers to be tested (aka. @controller)
- A TestRequest to simulate a web request (aka. @request)
- A TestResponse to provide information about the test request (aka. @response)
99% if not 100% of your functional tests will have these 3 objects in the setup.
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. get method 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!'}
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]
assigns["something"] assigns(:something) # because you can’t use assigns[:something] for historical reasons
Keep an eye out for that. mmmm kay?
9.4 Response-Related Assertions
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. 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_response :success # page rendered ok
assert_response :redirect # we've been redirected
assert_response :missing # not found
assert_response 505 # status code was 505
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
9.5 Tag-Related Assertions
The assert_tag and assert_no_tag assertions are for analysing the html returned from a request.
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/ }
assert_no_tag ( options )
This is the exact opposite of assert_tag. It ensures that the tag does not exist.
9.6 Routing-Related Assertions
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
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'
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