7.3. Testing in RailsRails 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 TestsIn Rails, these three types of tests have very specific meanings that may differ from what you expect:
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:
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. EnvironmentsWe 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 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:
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 book, 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 section 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. FixturesFixtures 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 testsIn 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:
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 testsIn 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 section, 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. Table 7-2 shows all of the extra assertions provided by Rails. Table 7-2. Rails-supplied assertions
7.3.1.5. Integration testsIntegration 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:
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 TestingRails 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. ZenTestSelf-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. SeleniumSelenium 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 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. |