Flylib.com

Books Software

 
 
 

Recipe 7.20. Writing Custom Assertions


Recipe 7.20. Writing Custom Assertions

Problem

As your test suite grows, you find that you need assertions that are specific to your applications. You can, of course, create the tests you need with the standard assertions (it's just code), but you'd rather create custom assertions for tests that you use repeatedly. There's no need to repeat yourself in your tests.

Solution

Define a method in test_helper.rb . For example, you might find that you're writing many test methods that test whether a book's ISBN is valid. You want to create a custom assertion named assert_valid_isbn to perform this test. Add the method to ./test/test_helper.rb :

test/test_helper.rb :

ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'test_help'

class Test::Unit::TestCase
  self.use_transactional_fixtures = true
  self.use_instantiated_fixtures  = false

def assert_valid_isbn(isbn)
    assert(/^\d{9}[\dxX]$/.match(isbn.to_s), "ISBN is invalid")
  end

end

You can now use your custom assertion in any of your tests.

test/unit/book_test.rb :

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

class BookTest < Test::Unit::TestCase
  fixtures :books

  def test_truth

assert_valid_isbn(1111111)

end
end

Discussion

assert_valid_isbn is a wrapper around the assert method. The method body asserts that the argument passed in matches the Regexp object defined between by the contents of "//". If the match method of Regexp returns a MatchData object, the assertion succeeds. Otherwise it fails, and the second argument of assert is displayed as the error message.

The solution demonstrates the utility of defining custom assertions that might otherwise become a maintenance problem. For example, in January 2007, the current 10-digit ISBN will officially be replaced by a 13-digit identifier. You'll eventually need to modify your application to take this into account, and you'll need to test the new application. That modification will be a lot easier if you've centralized "knowledge" of the ISBN's format in one place, so you only have to change it once.

Even if you don't anticipate the code in your assertions to change, custom assertions can avoid code duplication. If you've got an assertion that contains complex logic, use assert_block method of the Test::Unit::Assertions module to test whether a block of code yields TRue or not. assert_block takes an error message as an argument and is passed a block of code to be tested . The format for assert_block is:

assert_block(message="assert_block failed.") { ...}

See Also

  • RDoc on Test::Unit Assertions , http://www.ruby-doc.org/stdlib/libdoc/test/unit/rdoc/classes/Test/Unit/Assertions.html



Recipe 7.21. Testing File Upload

Problem

Contributed by: Evan Henshaw-Plath (rabble)

Your have an application that processes files submitted by users. You want a way to test the file-uploading functionality of your application as well as its ability to process the contents of the uploaded files.

Solution

You have a controller that accepts files as the :image param and writes them to the ./public/images/ directory from where they can later be served . A display message is set accordingly , whether or not saving the @image object is successful. (If the save fails, @image.errors will have a special error object with information about exactly why it failed to save.)

app/controllers/image_controller.rb :

def upload
  @image = Image.new(params[:image])
  if @image.save
    notice[:message] = "Image Uploaded Successfully"
  else 
    notice[:message] = "Image Upload Failed"
  end
end

Your Image model schema is defined by:

ActiveRecord::Schema.define() do
  create_table "images", :force => true do t
    t.column "title", :string, :limit => 80
    t.column "path", :string
    t.column "file_size", :integer
    t.column "mime_type", :string
    t.column "created_at", :datetime
  end
end

The Image model has an attribute for image_file but is added manually and will not be written in to the database. The model stores only the path to the file, not its contents. It writes the File object to a actual file in the ./public/images/ directory and it extracts information about the file, such as size and content type.

app/model/image_model.rb :

class Image < ActiveRecord::Base

  attr_accessor :image_file
  validates_presence_of :title, :path
  before_create :write_file_to_disk
  before_validation :set_path

  def set_path
    self.path = "#{RAILS_ROOT}/public/images/#{self.title}"
  end

  def write_file_to_disk
    File.open(self.path, 'w') do f
      f.write image_file.read
    end
  end
end

To test uploads, construct a post where you pass in a mock file object, similar to what the Rails libraries do internally when a file is received as part of a post:

test/functional/image_controller_test.rb :

require File.dirname(__FILE__) + '/../test_helper'
require 'image_controller'
      
# Re-raise errors caught by the controller.
class ImageController; def rescue_action(e) raise e end; end
         
class ImageControllerTest < Test::Unit::TestCase
  def setup
    @controller = ImageController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
  end
      
    
  def test_file_upload
    post :upload, {
      :image => {
         :image_file => uploadable_file('test/mocks/image.jpg',
                                        'image/jpeg'),
         :title => 'My Test Image'
      }
    }
    
    assert_kind_of? Image, assigns(:image),
      'Did @image get created with a type of Image'
    assert_equal 'My Test Image', assigns(:image).title,
      'Did the image title get set?'
  end
end

You must create a mock file object that simulates all the methods of a file object when it's uploaded via HTTP. Note that the test expects a file called image.jpg to exist in your application's test/mocks/ directory.

Next, create the following helper method that will be available to all your tests:

test/test_helper.rb :

ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'test_help'

class Test::Unit::TestCase
  self.use_transactional_fixtures = true

  def uploadable_file( relative_path, 
                       content_type="application/octet-stream", 
                       filename=nil)

    file_object = File.open("#{RAILS_ROOT}/#{relative_path}", 'r')

    (class << file_object; self; end;).class_eval do
       attr_accessor :original_filename, :content_type
    end

    file_object.original_filename = 
      File.basename("#{RAILS_ROOT}/#{relative_path}")

    file_object.content_type = content_type

    return file_object
  end
end

Discussion

Rails adds special methods to the file objects that are created via an HTTP POST. To properly test file uploads you need to open a file object and add those methods. Once you upload a file, by default, Rails places it in the /tmp/ directory. Your controller and model code will need to take the file object and write it to the filesystem or the database.

File uploads in Rails are passed in simply as one of the parameters in the params hash. Rails reads in the HTTP POST and CGI parameters and automatically creates a file object. It is up your controller to handle that file object and write it to a file on disk, place it in the database, or process and discard it.

The convention is that you store files for tests in the ./test/mocks/test/ directory. It's important that you have routines that clean up any files that are saved locally by your tests. You should add a teardown method to your functional tests that performs this task.

The following example shows how you can add a custom clean-up method, which deletes any image files you may have previously uploaded. teardown , like setup , is called for each test method in the class. We know from the above that all images are getting written to the ./public/images/ directory, so we just need to delete everything from that directory after each test. teardown is run regardless of whether the test passes or fails.

test/functional/image_controller_test.rb :

def teardown
  FileUtils.rm_r "#{RAILS_ROOT}/public/backup_images/", :force => true
  FileUtils.mkdir "#{RAILS_ROOT}/public/backup_images/"
end

See Also

  • Section 14.8"

  • Section 15.2"