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