Contents 

Ruby on Rails:
Table of Contents
Preface
Zero to Sixty: Introducing Rails
1.1. Rails Strengths
1.2. Putting Rails into Action
1.3. Organization
1.4. The Web Server
1.5. Creating a Controller
1.6. Building a View
1.7. Tying the Controller to the View
1.8. Under the Hood
1.9. What's Next?
Active Record Basics
2.1. Active Record Basics
2.2. Introducing Photo Share
2.3. Schema Migrations
2.4. Basic Active Record Classes
2.5. Attributes
2.6. Complex Classes
2.7. Behavior
2.8. Moving Forward
Active Record Relationships
3.1. belongs_to
3.2. has_many
3.3. has_one
3.4. What You Haven't Seen
3.5. Looking Ahead
Scaffolding
4.1. Using the Scaffold Method
4.2. Replacing Scaffolding
4.3. Generating Scaffolding Code
4.4. Moving Forward
Extending Views
5.1. The Big Picture
5.2. Seeing Real Photos
5.3. View Templates
5.4. Setting the Default Root
5.5. Stylesheets
5.6. Hierarchical Categories
5.7. Styling the Slideshows
Ajax
6.1. How Rails Implements Ajax
6.2. Playing a Slideshow
6.3. Using Drag-and-Drop to Reorder Slides
6.4. Drag and Drop Everything (Almost Everything)
6.5. Filtering by Category
Testing
7.1. Background
7.2. Ruby's Test::Unit
7.3. Testing in Rails
7.4. Wrapping Up
Installing Rails
1.1. Windows
2.1. OS X
3.1. Linux
Quick Reference
5.1. General
5.2. Testing
5.3. RJS (Ruby JavaScript)
5.4. Active Record
5.5. Controllers
5.6. Views
5.7. Ajax
5.8. Configuring Your Application
About the Authors
Colophon
Index
A
B
C
D
E
F
G
H
I
J
L
M
N
O
P
R
S
T
U
V
W
X
Y
Z

Ruby on Rails manual

Prev Page Next Page
Previous Page
Next Page

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. "#rubyrails-chp-7-fn-1">[*] 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:

[*] Only the YAML format allows you to name a fixture, so if you use the CSV format, you will not be able to do this.

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:

  1. 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.

  2. 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.

  3. 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.


Previous Page
Next Page