Ruby on Rails 2.1 - Unit Testing


Advertisements

Introduction

Before proceeding, let's have a quick look at a few definitions −

  • The Tests − They are test applications that produce consistent result and prove that a Rails application does what it is expected to do. Tests are developed concurrently with the actual application.

  • The Assertion − This is a one line of code that evaluates an object (or expression) for expected results. For example – Is this value = that value? Is this object nil?

  • The Test Case − This is a class inherited from Test::Unit::TestCase containing a testing strategy comprised of contextually related tests.

  • The Test Suite − This is a collection of test cases. When you run a test suite, it will, in turn, execute each test that belongs to it.

Rails Testing

When you run the helper script script/generate to create controllers and models, Rails generate a framework for unit and functional tests. You can get pretty good test coverage by filling in the framework with tests for the functionality you write. There are two important points to test in a Rails application −

  • Testing the Models

  • Testing the Controllers

This tutorial will cover both the testings in brief. So let's create one testapp to understand the concept.

C:\ruby> rails -d mysql testapp

Database Setup

Till now, we have used only Rails application's development database, but now you need to make sure that the testing database is also created and appropriate sections of your config/database.yml file are set up correctly.

Let's create development and testing databases as follows −

mysql> create database testapp_test;
Query OK, 1 row affected (0.01 sec)

mysql> create database testapp_development;
Query OK, 1 row affected (0.01 sec)

mysql> use testapp_test;
Database changed

mysql> grant all privileges on testapp_test.* 
   to 'root'@'localhost' identified by 'password';
Query OK, 0 rows affected (0.00 sec)

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)

Configuring database.yml

Configure your config/database.yml as follows −

development:
   adapter: mysql
   encoding: utf8
   database: testapp_development
   username: root
   password: password
   host: localhost
test:
   adapter: mysql
   encoding: utf8
   database: testapp_test
   username: root
   password: password
   host: localhost
production:
   adapter: mysql
   encoding: utf8
   database: testapp_production
   username: root
   password: password
   host: localhost

Generate Migration

Assume you have a table containing books, including their titles, price, and a small description. The following migration sets up this table −

testapp > ruby script/generate migration books

Now modify the testapp/db/migrate/20080616170315_books.rb file as follows −

class Books < ActiveRecord::Migration
   def self.up
      create_table :books do |t|
         t.string     :title, :limit => 32, :null => false
         t.float      :price
         t.text       :description
         t.timestamp  :created_at
      end
   end
  
   def self.down
      drop_table :books
   end
end

Now run the migration as follows −

testapp > rake db:migrate

This will create books table in testapp_development database. Thereafter, we need to set up your test database using rake command as follows −

C:\ruby\testapp > rake db:test:clone_structure

This will clone the testapp_development database into testapp_test database. It means whatever you have in the development database, now you will have the same data in the test database as well.

Testing Models

When you generate a model with the generate script, Rails also generates a unit test script for the model in the test directory. It also creates a fixture, a YAML file containing test data to be loaded into the testapp_test database. This is the data against which your unit tests will run −

testapp > ruby script/generate model Book
   exists  app/models/
   exists  test/unit/
   exists  test/fixtures/
   create  app/models/book.rb
   create  test/unit/book_test.rb
   create  test/fixtures/books.yml
   create  db/migrate
   create  db/migrate/20080616164236_create_books.rb

As you write code in the model classes, you'll write corresponding tests in these files. So let's create two test book records using YAML in test/fixtures/books.yml as follows −

perl_cb:
   id: 1
   title: 'Ruby Tutorial'
   price: 102.00
   description : 'This is a nice Ruby tutorial'
java_cb:
   id: 2
   title: 'Java Programming'
   price: 62.00
   description : 'Java Programming for the beginners'

Now let's replace the existing code in book unit test file test/unit/book_test.rb with the following code −

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

class BookTest < ActiveSupport::TestCase
   fixtures :books

   def test_book

      perl_book = Book.new :title => books(:perl_cb).title, 
         :price => books(:perl_cb).price,
         :description => books(:perl_cb).description,
         :created_at => books(:perl_cb).created_at

      assert perl_book.save

      perl_book_copy = Book.find(perl_book.id)

      assert_equal perl_book.title, perl_book_copy.title

      perl_book.title = "Ruby Tutorial"

      assert perl_book.save
      assert perl_book.destroy
   end
end

Finally, run the test method as follows −

testapp > ruby test/unit/book_test.rb

Here's the output of running the successful test case −

testapp > ruby test/unit/book_test_crud.rb 
Loaded suite ./test/unit/book_test
Started
.
Finished in 0.0625 seconds.

1 tests, 4 assertions, 0 failures, 0 errors

Let’s analyze what happened here −

  • The BookTest method starts off by creating a new Book object using the title and other fields from the first record in the text fixture/books.yml. The resulting object is stored in the perl_book instance variable.

  • The first assertion tests that saving the Book object was successful.

  • Next, the book object is retrieved using the find method and stored in another instance variable named perl_book_copy. The success of this retrieval is tested in the next assertion, which compares the titles of both book objects. At this point, we've tested the ability to create and read a database record.

  • The solution tests updating by assigning a new title to the object stored in perl_book and then asserts that saving the change is successful.

  • Finally, the ability to destroy a Book object is tested.

This is how we can test our Rails Models.

Testing the Controllers

Controller testing is also known as functional testing. Functional testing tests the following type of functionalities of the controllers −

  • Is the response redirected as expected?
  • Is the expected template rendered?
  • Is the routing as expected?
  • Does the response contain the expected tags?

Rails framework supports five types of requests −

  • get
  • post
  • put
  • head
  • delete

To write a functional test, you need to simulate any of the five HTTP request types that your controller will process.

Request type "get" and "post" are the most commonly used in controller testing. All these methods take four arguments −

  • The action of a controller
  • An optional hash of request parameters
  • An optional session hash
  • An optional flash hash

In this tutorial, we will see how to use get method to test our controller. You can test rest of the methods in similar way.

When you generate a controller with generate, Rails creates a functional test script for the controller as follows −

testapp > ruby script/generate controller Book
   exists  app/controllers/
   exists  app/helpers/
   create  app/views/book
   exists  test/functional/
   create  app/controllers/book_controller.rb
   create  test/functional/book_controller_test.rb
   create  app/helpers/book_helper.rb

As you write code in the controller classes, you'll write corresponding tests in these files. Before that, let's define our controller functions list, show, and search inside app/controllers/book_controller.rb as follows −

class BookController < ApplicationController
   def list
      @book_pages, @books = paginate :books, :per_page => 10
   end

   def show
      @book = Book.find(params[:id])
   end

   def search
      @book = Book.find_by_title(params[:title])
      if @book
         redirect_to :action => 'show', :id => @book.id
      else    
         flash[:error] = 'No such book available'
         redirect_to :action => 'list'
      end
   end
end

NOTE − You would need two views templates for show and list method. You can define those views and test them, but right now, we will proceed without defining those views.

Now let's reuse our test fixture which is in the test/fixtures/books.yml file as follows −

perl_cb:
   id: 1
   title: 'Ruby Tutorial'
   price: 102.00
   description : 'This is a nice Ruby tutorial'
java_cb:
  id: 2
  title: 'Java Programming'
  price: 62.00
  description : 'Java Programming for the beginners'

Add the following test_search_book and test_search_not_found methods to test/functional/book_controller_test.rb to test the functionality of the Book Controller's search action.

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

# Re-raise errors caught by the controller.
class BookController
   def rescue_action(e) 
      raise e 
   end
end

class BookControllerTest < Test::Unit::TestCase
   fixtures :books
   def setup
      @controller = BookController.new
      @request    = ActionController::TestRequest.new
      @response   = ActionController::TestResponse.new
   end

   def test_search_book
      get :search, :title => 'Ruby Tutorial'
      assert_not_nil assigns(:book)
      assert_equal books(:perl_cb).title, assigns(:book).title
      assert_valid assigns(:book)
      assert_redirected_to :action => 'show'
   end

   def test_search_not_found
      get :search, :title => 'HTML Tutorial'
      assert_redirected_to :action => 'list'
      assert_equal 'No such book available', flash[:error]
   end
end

Now run your test cases as follows −

testapp > ruby test/functional/book_controller_test.rb 

It gives the following output −

Loaded suite test/functional/book_controller_test
Started
..
Finished in 0.422 seconds.

2 tests, 7 assertions, 0 failures, 0 errors

Let's analyze what has happened here −

  • The setup method is a default method to create controller, request, and response objects. They would be used by Rails internally.

  • The first test method test_search_book generates a get request to the search action, passing in a title parameter.

  • The next two assertions verify that a Book object was saved in an instance variable called @book and that the object passes any Active Record validations that might exist.

  • The final assertion inside first method tests that the request was redirected to the controller's show action.

  • The second test method, test_search_not_found, performs another get request but passes in an invalid title

  • The first assertions test that a redirect to the list action was issued.

  • If the proceeding assertions passed, there should be a message in the flash hash which you can test with assert_equal..

To obtain more information on Assertions, please refer Rails Standard Documentation.

Using Rake for Testing

You can use rake utility to test your applications. Given below is a list of few important commands.

  • $rake test − Test all unit tests and functional tests (and integration tests, if they exist).

  • $rake test:functionals− Run all functional tests.

  • $rake test:units − Run all unit tests.

  • $rake test:integration − Run all integration tests.

  • $rake test:plugins − Run all test in ./vendor/plugins/**/test.

  • $rake test:recent − Run tests for models and controllers that have been modified in the last 10 minutes −

  • $rake test:uncommitted − For projects in Subversion, run tests for the changes that took place in the models and controllers since the last commit −

Advertisements