7.3. Testing in
Rails
Rails extends the Test::Unit framework
to include new assertion methods that are specific to web
applications and to the Ruby on Rails framework. Rails also
provides explicit higher-level support for testing by including a
consistent method for loading test data and a mechanism for running
different types of test.
7.3.1. Unit Tests,
Functional Tests, and Integration Tests
In Rails, these three types of tests have very
specific meanings that may differ from what you expect:
-
Unit tests are for testing models.
-
Functional tests are for testing
controllers.
-
Integration tests are for testing higher-level
scenarios that exercise interactions between controllers.
Look at your Photo Share application's directory
tree, and you'll find that it contains a test subdirectory. All tests reside under this
test subdirectory, which has
several subdirectories of its own:
unit
-
Holds all unit tests.
functional
-
Holds all functional tests.
integration
-
Holds all integration tests.
fixtures
-
Contains sample data for all tests (more on this
later).
Take a look at photos/test/unit, and you'll see that it
already contains category_test.rb,
photo_test.rb, slide_test.rb, and slideshow_test.rb. These are test case
skeletons created by Rails when we generated our model classes. But
before you can start filling these skeleton test files, you first
need to understand Rails' environments
and fixtures.
7.3.1.1.
Environments
We software developers have always distinguished
between code running in some form of development mode versus
production mode. Development mode usually offers features such as
active debugging, logging, and array bounds checking. These all add
unnecessary overhead, so you should normally strip those
conveniences out of your delivered production code.
This distinction of development versus
production has usually been informal and ad hoc. As introduced in
"rubyrails-chp-2.html#rubyrails-chp-2">Chapter 2, Rails
formalizes this practice using what it calls environments. Rails comes with three
predefined environments: development, test, and production. You can
also define new environments if you like, but most developers
don't.
Each environment can have its own database and
runtime settings. For example, in production mode, you usually want
as much caching as possible to maximize performance, but in
development mode, you want all caching disabled so that you can
make a change and then immediately see it work. The predefined
Rails environments have the default settings that make sense for
each environment.
There are several ways to tell Rails what
environment to use:
-
Set the operating system environment variable
RAILS_ENV to 'development',
'production', or 'test'.
-
Specify the environment value in config/environment.rb with a line of Ruby code
like this: ENV['RAILS_ENV'] = 'production'.
-
Use the -e option on the
script/server script to start the WEBrick server. For
example, script/server -e production starts the web server
in production mode. Development mode is the default.
Take a look at the Photo Share application's
config/environments directory and
you will find three files: development.rb, test.rb, and production.rb. Each file contains the settings
for its environment. These default environments are pretty well
thought out, and it is unlikely that you will need to change them.
But you should change the database settings for each environment.
At the beginning of this site, we set up the development database,
and now we need to set up the test database. Edit config/database.yml, and make sure that the
test looks like this:
test:
adapter: mysql
database: photos_test
username: <your userid>
password: <your password>
socket: localhost
Start the mysql
command prompt (mysql -u <username> -p
<password>). Then, create a database called
photos_test:
mysql> create database photos_test;
Query OK, 1 row affected (0.05 sec)
Now we can use a built-in feature of Rails to
clone the database schema from the production database to the test
database. Open a console window, navigate to the root directory of
the Photo Share application, and run the command:
>rake db:test:clone_structure
You now have a test database that is identical
to the development database, except that the tables do not contain
any data. Getting data into these tables to use in our test is what
fixtures
are all about.
7.3.1.2.
Fixtures
Fixtures
contain test data that Rails loads into your models before
executing each test. You create your fixture data in the
test/fixtures directory, and they
can be in either CSV (comma-separated value) or YAML (YAML Ain't
Markup Language) format.
YAML is the preferred format because it is so
simple and readable, consisting mostly of keyword/value pairs. CSV
files are useful when you have existing data in a database or
spreadsheet that you can export to CSV format.
Fixtures for a particular database table should
have the same filename as the database table name. So, to have
fixtures for our photos database table, you would have a
photos.yml file in the
test/fixtures directory. Rails
created a placeholder photos.yml
when you created the photos model. Edit this existing test/fixtures/photos.yml file, and replace its
contents with this:
train_photo:
id: 1
filename: train.jpg
created_at: 2006-04-01 03:20:49
thumbnail: t_train.jpg
description: This is a cool train!
lighthouse_photo:
id: 2
filename: lighthouse.jpg
created_at: 2006-04-02 14:58:49
thumbnail: t_lighthouse.jpg
description: My favorite lighthouse.
YAML is sensitive to whitespace, so be sure to
use spaces instead of tabs, and eliminate any trailing spaces or
tabs. These same two fixtures in CSV format look like this in a
photos.csv file (in CSV
format):
id, filename, created_at, thumbnail, description
1, train.jpg, "2006-04-01 03:20:49", t_train.jpg, "This is a cool train!"
2, lighthouse.jpg, "2006-04-02 14:58:49", t_lighthouse.jpg, "My favorite"
In the YAML file, the first line of each fixture
is a name that is assigned to that fixture. (A little bit later,
you will see how you can use this name.) The remaining lines are
keyword/value pairs, one for each column in the database table.
Now that we have a test database and some
fixtures, we can actually start writing some tests.
7.3.1.3. Unit
tests
In Rails, unit tests are for testing
your models. The file photos/test/unit/photo_test.rb, for example,
is where to create tests to test the Photo model. Rails created a skeleton of this file when we
created the model. It currently looks like this:
require File.dirname(__FILE__) + '/../test_helper'
class PhotoTest < Test::Unit::TestCase
fixtures :photos
# Replace this with your real tests.
def test_truth
assert_kind_of Photo, photos(:first)
end
end
Let's walk through the code a line at a
time:
require File.dirname(__FILE__) +
'/../test_helper'
-
There are some serious Ruby idioms in this line
of code, but the net result is to instruct Ruby to require (load)
the file test_helper.rb from the
parent directory (photos/test).
test_helper.rb activates the Rails
environment so that our tests are ready to run. __FILE__
is a special Ruby constant that contains the full path of the
currently executing file. The File.dirname method takes
that full path and removes the filename, returning only the
directory path.
class PhotoTest <
Test::Unit::TestCase
-
This code makes the PhotoTest class a
subclass of Test::Unit::TestCase, as is required for
running tests using Test::Unit.
fixtures :photos
-
This code tells Rails to load sample photo data
into the database before each test (any existing data in the
database is purged first). You can load multiple fixtures in one
statement like this: fixtures :photos, :categories,
slideshows.
It's finally time to create and run our first
test. Edit photos/test/unit/photo_test.rb, and then add
this code in the place of test_truth:
def test_photo_count
assert_equal 3, Photo.count
end
This test is going to fail because it is
asserting that the Photo database table contains three rows, but
photos.yml contains only two. Lets
try it and see. Open a command prompt, navigate to the root
directory of our Photo Share application, and run this command:
> rake test:units
You should see the following output:
Started
.F..
Finished in 0.313 seconds.
1) Failure:
test_photo_count(PhotoTest) [./test/unit/photo_test.rb:7]:
<3> expected but was
<2>.
4 tests, 4 assertions, 1 failures, 0 errors
Remember that the test/units directory contains four test files
(even though we have modified only one of them), so this test ran
all four. As expected, our test failed. Let's fix that:
def test_photo_count
assert_equal 2, Photo.count
end
When you run the unit tests, you get:
Started
....
Finished in 0.359 seconds.
4 tests, 4 assertions, 0 failures, 0 errors
You know that fixtures are used to populate our
database tables. But you can also individually access each
fixture's data using the fixture's name.
photos(:train_photo).attributes returns a hash containing
all the keyword/value pairs for the TRain_photo fixture,
so photos(:train_photo).attributes['id'] returns the value
of the id property (which is 1). More
interestingly, you can retrieve an entire fixture's entry from the
database using its name:
photo = photos(:train_photo)
Retrieving the TRain_photo object from
the database by name is the equivalent to retrieving it by
id:
photo = Photo.find(1)
Let's use this feature to add another test to
photos/test/unit/photo_test.rb:
def test_photo_content
assert_equal photos(:train_photo).attributes['id'], 1
assert_equal photos(:train_photo), Photo.find(1)
assert_equal photos(:lighthouse_photo).attributes['id'], 2
assert_equal photos(:lighthouse_photo), Photo.find(2)
end
When you run the unit tests, you get:
Started
.....
Finished in 0.359 seconds.
5 tests, 8 assertions, 0 failures, 0 errors
Before we move on to functional tests, let's write one
more test that exercises our ability to perform basic CRUD
operations with our Photo model. Once again, edit photos/test/unit/photo_test.rb, and add:
def test_photo_crud
# create a new photo
cat = Photo.new
cat.filename = 'cat.jpg'
cat.created_at = DateTime.now
cat.thumbnail = 't_cat.jpg'
cat.description = 'This is my cat!'
# save it to the database
assert cat.save
# read it back from the database
assert_not_nil cat2 = Photo.find(cat.id)
# make sure they are the same
assert_equal cat, cat2
# modify this cat and update the database
cat2.description = 'A ghost of my cat.'
assert cat2.save
# delete it from the database
assert cat2.destroy
end
Let's run the test again and see whether this is
going to pass:
Started
......
Finished in 0.594 seconds.
6 tests, 13 assertions, 0 failures, 0 errors
With our guilt suitably assuaged, let's move on
to functional tests.
7.3.1.4.
Functional tests
In Rails, you'll use functional tests to
exercise one feature, or function, in your controllers. Functional
and integration tests check the responses to web commands, called
http requests. In this , we
work on functional tests for the photos controller.
We originally created our photos
controller by generating scaffolding for it. When you generate
scaffolding for a database table, Rails creates a remarkably
complete set of functional tests:
require File.dirname(__FILE__) + '/../test_helper'
require 'photos_controller'
# Reraise errors caught by the controller.
class PhotosController; def rescue_action(e) raise e end; end
class PhotosControllerTest < Test::Unit::TestCase
fixtures :photos
def setup
@controller = PhotosController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_index
get :index
assert_response :success
assert_template 'list'
end
def test_list
get :list
assert_response :success
assert_template 'list'
assert_not_nil assigns(:photos)
end
def test_show
get :show, :id => 1
assert_response :success
assert_template 'show'
assert_not_nil assigns(:photo)
assert assigns(:photo).valid?
end
def test_new
get :new
assert_response :success
assert_template 'new'
assert_not_nil assigns(:photo)
end
def test_create
num_photos = Photo.count
post :create, :photo => {}
assert_response :redirect
assert_redirected_to :action => 'list'
assert_equal num_photos + 1, Photo.count
end
def test_edit
get :edit, :id => 1
assert_response :success
assert_template 'edit'
assert_not_nil assigns(:photo)
assert assigns(:photo).valid?
end
def test_update
post :update, :id => 1
assert_response :redirect
assert_redirected_to :action => 'show', :id => 1
end
def test_destroy
assert_not_nil Photo.find(1)
post :destroy, :id => 1
assert_response :redirect
assert_redirected_to :action => 'list'
assert_raise(ActiveRecord::RecordNotFound) {
Photo.find(1)
}
end
end
These tests are in the file photos/test/functional/photos_controller_test.rb
and cover the full range of CRUD operations. The Rails-generated
functional tests for our other controllers are very similar.
You can run the functional tests with the
command rake test:functionals but be forewarned that you
will see a lot of errors! You might think that our Photo Share
application has many problems, but the problem is that our tests
are simply out of date. Those tests worked perfectly fine when they
were first created and we were using the scaffolding for
everything. But since that time, we have made lots of changes to
the code yet never changed the tests to keep up with the evolving
code base. Now we need to fix these tests.
For the purposes of this chapter, we are going
to get the photo controller's functional tests working to give you
enough understanding to fix the others yourself. To simplify the
test reports, move all functional tests in photos/test/functional, except for
photos_controller_test.rb, to
another directory for safe keeping.
Because you can assign every photo to one or
more categories, a lot of the photo controller code also works with
categories. But we don't yet have any test categories, only test
photos. So the first thing to do is to create some fixtures for the
categories table and the
categories_photos join table.
Edit the file photos/test/fixtures/categories.yml, and
replace its contents with this:
all:
id: 1
name: All
people:
id: 2
name: People
parent_id: 1
animals:
id: 3
name: Animals
parent_id: 1
things:
id: 4
name: Things
parent_id: 1
Now create the file photos/test/fixtures/categories_photos.yml
with this content:
train_category:
photo_id: 1
category_id: 4
lighthouse_category:
photo_id: 2
category_id: 4
Finally, edit photos/test/functional/photos_controller_test.rb,
and add these two lines at the beginning of the class definition
for CategoriesControllerTest:
fixtures :categories
fixtures :categories_photos
Let's try running our functional tests. From the
base directory of our Photo Share application, run this
command:
> rake test:functionals
Started
F.......
Finished in 0.469 seconds.
1) Failure:
test_create(PhotosControllerTest) [./test/functional/photos_controller_test.rb:5
5]:
Expected response to be a <:redirect>, but was <200>
8 tests, 25 assertions, 1 failures, 0 errors
Hmmm: that wasn't exactly error-free; there was
an assertion failure in the method test_create:
def test_create
num_photos = Photo.count
post :create, :photo => { }
assert_response :redirect
assert_redirected_to :action => 'list'
assert_equal num_photos + 1, Photo.count
end
This test tries to create a new photo by posting
a request to the create action of the current controller
(which is the photo controller). We expected that the create action
would save a new photo to the database and then redirect to the
list action. Instead, we got an http 200 response (which is a normal,
everything's OK, response).
A quick look at the create method shows
that if the save to the database fails, then the controller renders
and returns the new template, which correctly returns an
http 200 response:
def create
@photo = Photo.new(params[:photo])
@photo.categories = Category.find(params[:categories]) if params[:categories]
if @photo.save
flash[:notice] = 'Photo was successfully created.'
redirect_to :action => 'list'
else
@all_categories = Category.find(:all, :order=>"name")
render :action => 'new'
end
end
Why would the save to the database
(@photo.save) fail? Let's take a look at the photo model
(photos/app/models/photo.rb) to
see whether that gives us any idea:
class Photo < ActiveRecord::Base
has_many :slides
has_and_belongs_to_many :categories
validates_presence_of :filename
end
If you look closely, you'll see the culprit
within the validation: validates_presence_of :filename.
This code will refuse to save any instance of Photo to the
database if it does not contain a filename; our test did not assign
a filename. To fix that problem, edit photos/test/functional/photos_controller_test.rb
to look like this:
def test_create
num_photos = Photo.count
post :create, :photo => {:filename => 'myphoto.jpg'}
assert_response :redirect
assert_redirected_to :action => 'list'
assert_equal num_photos + 1, Photo.count
end
When you run the functional tests again, you'll
see:
> rake test:functionals
Started
........
Finished in 0.468 seconds.
8 tests, 28 assertions, 0 failures, 0 errors
Excellent. All the functional tests for the
photos controller are now succeeding.
Did you notice that functional tests for the
photos controller use a lot of assertions that are not part of
Test::Unit but seem to be specific to web development
(assert_redirected_to) and even specific to Rails
(assert_template)? Rails provides these additional
assertions.
"#rubyrails-chp-7-table-2">Table 7-2 shows all of the extra
assertions provided by Rails.
Table 7-2. Rails-supplied assertions
|
Assertion
|
Description
|
|
assert_dom_equal
assert_dom_not_equal
|
Asserts that two HTML strings are logically
equivalent.
|
|
assert_generates
|
Asserts that the provided options can generate
the provided path.
|
|
assert_tag
|
Asserts that there is a tag/node/element in the
body of the response that meets all the given conditions.
|
|
assert_recognizes
|
Asserts that the routing rules successfully
parse the given URL path.
|
|
assert_redirected_to
|
Asserts that the response is a redirect to the
specified destination.
|
|
assert_response
|
Asserts that the response was the given HTTP
status code (or range of status codes).
|
|
assert_routing
|
Asserts that path (URL) and options match both
ways.
|
|
assert_template
|
Asserts that the request was rendered with the
specified template file.
|
|
assert_valid
|
Asserts that the provided record is valid by
active record standards.
|
7.3.1.5.
Integration tests
Integration tests are a new feature in Rails
1.1. Integration tests are higher-level scenario tests that verify
the interactions between the application's actions, across all
controllers.
As you might have guessed by now, integration
tests live in the test/integration
directory and are run using the command rake
test:integration.
Our Photo Share application hasn't yet been
developed to the point where integration tests would be useful.
Here, instead, is a hypothetical integration test to give you a
feel for what they are like:
require "#{File.dirname(__FILE__)}/../test_helper"
class UserManagementTest < ActionController::IntegrationTest
fixtures :users, :preferences
def test_register_new_user
get "/login"
assert_response :success
assert_template "login/index"
get "/register"
assert_response :success
assert_template "register/index"
post "/register",
:user_name => "happyjoe",
:password => "neversad"
assert_response :redirect
follow_redirect!
assert_response :success
assert_template "welcome"
end
This test leads its application through the
series of web pages that a new user would go through to register
with the site. You can see that the scenario being tested is pretty
easy to follow:
-
Send an HTTP GET request for the /login page. Now check to see whether the
request was successful and whether the response was rendered by the
expected template.
-
Simulate the user clicking on the "register"
button or link by sending an HTTP GET request for the /register page. Again, check for the proper
response.
-
Simulate the new user filling out and submitting
the registration form by sending an HTTP POST request that includes
user_name and password field values. Now verify
that the response is a redirect, follow the redirect, and verify
that you successfully end up on the welcome page.
Integration tests can be used to duplicate bugs
that have been reported. Then, when you fix the bug, you will know
it because your test will start succeeding. Plus, you then have a
test in place that will alert you if the same bug ever
reappears.
7.3.2. Advanced
Testing
Rails provides an impressive level of support
for testing. But just in case that's not enough for you, here are a
couple of third-party testing tools that are really on the cutting
edge and worthy of your attention.
7.3.2.1.
ZenTest
Self-described as "testing on steroids," ZenTest
provides a set of integrated testing tools to automate and
streamline your testing. For example, autotest monitors
your projects files for changes. When autotest detects a change, it automatically
runs the appropriate test to verify that the change has not broken
anything.
You can learn more about ZenTest at http://www.zenspider.com/ZSS/Products/ZenTest/.
7.3.2.2.
Selenium
Selenium is a testing tool written specifically
for web applications. Selenium tests run directly in a browser,
just as real applications do, provided it's a modern browser that
supports JavaScript. As such, it is an ideal tool for testing the
Ajax features of a web application.
You can learn more about Selenium on its home
page at openqa.org/selenium/">http://www.openqa.org/selenium/.
IBM's developerWorks has a good article on using Selenium with Ruby
on Rails at the following address:
http://www-128.ibm.com/developerworks/java/library/wa-selenium-ajax/index.html.
 |