Search code examples
ruby-on-railsrubytestingfile-uploadshrine

Rails: Testing file upload validation (Shrine gem) at model spec


The following code tests image validation within a model spec in a Rails 4.2 app with RSpec 3.5 and the Shrine gem for file uploads.

My questions are:

  • Can you think of a way to improve the following tests or a better way to test those validations?
  • How to improve the speed of the file size validation test? If possible, I'd like to test it without actually having to upload a >10mb file.

Other aspects of the file upload setup are tested in controller and feature specs, which are irrelevant to this question.

RSpec.describe ShareImage, :type => :model do
  describe "#image", :focus do
    let(:image_file) do
      # Could not get fixture_file_upload to work, but that's irrelevant
      Rack::Test::UploadedFile.new(File.join(
        ActionController::TestCase.fixture_path, 'files', filename))
    end
    let(:share_image) { FactoryGirl.build(:share_image, image: image_file) }
    before(:each) { share_image.valid? }

    context "with a valid image file" do
      let(:filename) { 'image-valid.jpg' }
      it "attaches the image to this record" do
        expect(share_image.image.metadata["filename"]).to eq filename
      end
    end

    context "with JPG extension and 'text/plain' media type" do
      let(:filename) { 'image-with-text-media-type.jpg' }
      it "is invalid" do
        expect(share_image.errors[:image].to_s).to include("invalid file type")
      end
    end

    # TODO: Refactor the following test (it takes ~50 seconds to run)
    context "with a >10mb image file" do
      let(:filename) { 'image-11mb.jpg' }
      it "is invalid" do
        expect(share_image.errors[:image].to_s).to include("too large")
      end
    end
  end
end

Solution

  • I would recommend that you test metadata extraction and validation separately. The metadata extraction you need to test with real IOs, but for validation tests you can assign a cached file with the desired metadata which doesn't have to actually exist.

    RSpec.describe ImageUploader do
      def uploaded_file(metadata = {})
        Shrine.uploaded_file(
          "id"       => "123",
          "storage"  => "cache",
          "metadata" => {"mime_type" => "image/jpeg", "size" => 100}.merge(metadata)
        )
      end
    
      let(:share_image) do
        FactoryGirl.build(:share_image, image: uploaded_file(metadata).to_json)
      end
    
      let(:metadata) { Hash.new }
    
      describe "validations" do
        before(:each) { share_image.valid? }
    
        context "when image is correct" do
          it "passes" do
            expect(share_image.errors).to be_empty
          end
        end
    
        context "when extension is correct but MIME types isn't" do
          let(:metadata) { Hash["filename" => "image.jpg", mime_type => "text/plain"] }
    
          it "fails" do
            expect(share_image.errors[:image].to_s).to include("isn't of allowed type")
          end
        end
    
        context "when file is larger than 10MB" do
          let(:metadata) { Hash["size" => 11 * 1024 * 1024] }
    
          it "fails" do
            expect(share_image.errors[:image].to_s).to include("too large")
          end
        end
      end
    end