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?
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. 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
9.5. Tag-Related Assertions
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. Routing-Related Assertions
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
