156

I want to test a file upload in rails, but am not sure how to do this.

Here is the controller code:

def uploadLicense
    #Create the license object
    @license = License.create(params[:license]) 


    #Get Session ID
    sessid = session[:session_id]

    puts "\n\nSession_id:\n#{sessid}\n"

    #Generate a random string
    chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
    newpass = ""
    1.upto(5) { |i| newpass << chars[rand(chars.size-1)] }

    #Get the original file name
    upload=params[:upload]
    name =  upload['datafile'].original_filename 

    @license.format = File.extname(name)

    #calculate license ID and location
    @license.location = './public/licenses/' + sessid + newpass + name 

    #Save the license file
    #Fileupload.save(params[:upload], @license.location) 
    File.open(@license.location, "wb") { |f| f.write(upload['datafile'].read) }

     #Set license ID
    @license.license_id = sessid + newpass

    #Save the license
    @license.save

    redirect_to :action => 'show', :id => @license.id 
end

I have tried this spec, but it doesnt work:

it "can upload a license and download a license" do
    file = File.new(Rails.root + 'app/controllers/lic.xml')
    license = HashWithIndifferentAccess.new
    license[:datafile] = file
    info = {:id => 4}
    post :uploadLicense, {:license => info, :upload => license}
end

How can I simulate the file upload, using rspec?

user727403
  • 1,967
  • 3
  • 13
  • 14

6 Answers6

308

You can use fixture_file_upload method to test file uploading: Put your test file in "{Rails.root}/spec/fixtures/files" directory

before :each do
  @file = fixture_file_upload('files/test_lic.xml', 'text/xml')
end

it "can upload a license" do
  post :uploadLicense, :upload => @file
  response.should be_success
end

In case you were expecting the file in the form of params['upload']['datafile']

it "can upload a license" do
  file = Hash.new
  file['datafile'] = @file
  post :uploadLicense, :upload => file
  response.should be_success
end
Mauricio Gracia Gutierrez
  • 10,288
  • 6
  • 68
  • 99
ebsbk
  • 4,512
  • 3
  • 24
  • 29
  • 31
    See http://bit.ly/OSrL7R (stack overflow question 3966263) if you are getting file does not exist errors. A different form is needed in Rails 3.2: @file = Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/files/test.csv'), 'text/csv') – Mike Blyth Sep 03 '12 at 10:04
  • 3
    fixture_file_upload also works if you specify the full path to the file: "Rails.root.join('spec/fixtures/files/test.csv" – jmanrubia Apr 11 '13 at 10:37
  • 1
    fixture_file_upload is being treated as a string in params, anyone know why? – Abe Petrillo Aug 27 '13 at 14:17
  • It's best practice to not rely on the state of member vars like `@file` in specs. Instead, you can set a `let!` (here the `!` means it's invoked in an implicit `before` block) like this: `let! :file_fixture do fixture_file_upload(...) end` and then in the post body do: `file['datafile'] = file_fixture`. Source: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let – acobster Sep 14 '15 at 20:41
  • 4
    @AbePetrillo (or whoever sees the comment and has the same question) I had the same issue. In my case, the first argument to `post` was a path helper method, whose only intended argument I didn't enclose in parentheses, so the following tokens were interpreted as additional args to the helper, rather than args for the post itself. E.g., I had `post order_documents_path @order, document: file` instead of `post order_documents_path(@order), document: file`. – soupdog Mar 16 '16 at 00:50
  • Gave me the **"file does not exist"** error. Fixed it by adding `include ActionDispatch::TestProcess` into the `RSpec.configure` block. – Yury Kaspiarovich Oct 22 '17 at 22:03
57

I am not sure if you can test file uploads using RSpec alone. Have you tried Capybara?

It's easy to test file uploads using capybara's attach_file method from a request spec.

For example (this code is a demo only):

it "can upload a license" do
  visit upload_license_path
  attach_file "uploadLicense", /path/to/file/to/upload
  click_button "Upload License"
end

it "can download an uploaded license" do
  visit license_path
  click_link "Download Uploaded License"
  page.should have_content("Uploaded License")
end
Ken
  • 7,847
  • 1
  • 21
  • 20
  • 7
    Of course this works in an integration spec. OP's question is concerning a controller unit spec, considering he's only posting controller code. Just saying if anyone's confused. Do this in a feature spec, do ebsbk's answer in a unit spec. – Starkers Jun 28 '14 at 13:44
  • 2
    The file path needs to be in quotes – sixty4bit Jul 11 '15 at 17:23
35

if you include Rack::Test*, simply include the test methods

describe "my test set" do
  include Rack::Test::Methods

then you can use the UploadedFile method:

post "/upload/", "file" => Rack::Test::UploadedFile.new("path/to/file.ext", "mime/type")

*NOTE: My example is based on Sinatra, which extends Rack, but should work with Rails, which also uses Rack, TTBOMK

zedd45
  • 2,101
  • 1
  • 31
  • 34
  • 3
    FYI: You don't necessarily have to `include Rack::Test::Methods` to use Rack::Test::UploadedFile. `require 'rack/test` is enough. – xentek Oct 25 '13 at 21:44
  • 4
    I din't even have to `require 'rack/test'`. if you are using `Rack::Test::UploadedFile` that's good enough to use it. Provided your rails app setup is fine. PS: I'm on `Rails 4` and `ruby 2.1` – Vishnu Narang Oct 02 '14 at 07:38
  • Vishnu's comment is the most accurate, as he is requiring the method explicitly. Including `rack/test` will include everything from test, which includes `test/methods`, but also includes *everything* in test, so probably more than you need. – zedd45 Mar 28 '16 at 13:48
19

I haven't done this using RSpec, but I do have a Test::Unit test that does something similar for uploading a photo. I set up the uploaded file as an instance of ActionDispatch::Http::UploadedFile, as follows:

test "should create photo" do
  setup_file_upload
  assert_difference('Photo.count') do
    post :create, :photo => @photo.attributes
  end
  assert_redirected_to photo_path(assigns(:photo))
end


def setup_file_upload
  test_photo = ActionDispatch::Http::UploadedFile.new({
    :filename => 'test_photo_1.jpg',
    :type => 'image/jpeg',
    :tempfile => File.new("#{Rails.root}/test/fixtures/files/test_photo_1.jpg")
  })
  @photo = Photo.new(
    :title => 'Uploaded photo', 
    :description => 'Uploaded photo description', 
    :filename => test_photo, 
    :public => true)
end

Something similar might work for you also.

Dave Isaacs
  • 4,499
  • 6
  • 31
  • 44
17

This is how I did it with Rails 6, RSpec and Rack::Test::UploadedFile

describe 'POST /create' do
  it 'responds with success' do
    post :create, params: {
      license: {
        picture: Rack::Test::UploadedFile.new("#{Rails.root}/spec/fixtures/test-pic.png"),
        name: 'test'
      }
    }

    expect(response).to be_successful
  end
end

DO NOT include ActionDispatch::TestProcess or any other code unless you're sure about what you're including.

thisismydesign
  • 21,553
  • 9
  • 123
  • 126
3

I had to add both of these includes to get it working:

describe "my test set" do
  include Rack::Test::Methods
  include ActionDispatch::TestProcess
nfriend21
  • 2,170
  • 1
  • 21
  • 21
  • 2
    Never ever include ActionDispatch::TestProcess explanation: https://github.com/honeybadger-io/honeybadger-ruby/blob/a18b1c3c4d912f96f1a3cb1435cc8a50e436f7f5/lib/honeybadger/notice.rb#L487 – gotar Nov 07 '18 at 13:23
  • Do not include ActionDispatch::TestProcess as explained here https://github.com/thoughtbot/factory_bot/issues/385#issuecomment-45065515 – Francisco Quintero Nov 18 '20 at 15:01