Using Rails test fixtures with CarrierWave

After posting a tweet about using fixtures a while ago, @stravid replied and asked me for some pointers to using fixtures in CarrierWave, the Rails file uploading gem, so I decided to dive in and see if I could get it to work. Here's how I would do it.

When using CarrierWave you create an uploader, which subclasses from CarrierWave::Uploader::Base, add a field to the database that holds the upload's filename, and link it all together using a call to mount_uploader in the model. So, when uploading avatars for users, for example, you'd create an AvatarUploader (there's a generator for that), add a string column in your users table named "avatar", and use it in your model like this:

# app/models/user.rb

class User < ActiveRecord::Base
  mount_uploader :avatar, AvatarUploader
end

Now, how would we test this? Let's put an image in test/fixtures/files/tapir.jpg, and use fixture_file_upload to test the uploader. Here's a test that checks if an existing user has an avatar, and one to make sure an avatar can be created with a new user:

# test/models/user_test.rb

require_relative '../test_helper'

class UserTest < ActiveSupport::TestCase
  test "has an avatar" do
    user = users(:user_with_avatar)
    assert File.exists?(user.avatar.file.path)
  end

  test "uploads an avatar" do
    user = User.create!(:avatar, fixture_file_upload('/files/tapir.jpg', 'image/jpg'))
    assert(File.exists?(user.reload.avatar.file.path))
  end
end

The first test uses a fixture named users(:user_with_avatar), so let's create that first. When you upload a file, only its basename gets stored in the User#avatar field, and the rest of the path to the file comes from your uploader class, meaning a fixture would look like this:

# test/fixtures/users.yml

user_with_avatar: # generated id: 605975481
  avatar: 'tapir.jpg'

Now, if you want to get "uploaded" files from the fixtures directory instead of having to upload it before every test, you can change CarrierWave.root in your test helper so it points to the fixture directory instead of the project's public directory:

# test/test_helper.rb

CarrierWave.root = 'test/fixtures/files'

Then, putting a file in test/fixtures/uploads/user/avatar/605975481/tapir.jpg (where "605975481" is the user's autogenerated ID), will make sure CarrierWave can find the fixture user's avatar in your tests.

Both tests should pass right now, but there's a problem. The second test uploads a new file directly to the fixture directory, which is not where you want it. What you actually want is to save the uploaded files to a temporary location, so the files created by your tests won't make a mess out of your fixture files.

After looking through CarrierWave's source for a while, I found that it actually already does this. When you upload a file and don't save it, CarrierWave will keep it in a temporary files directory until you save the parent model instance, which then moves the file to your uploader's store_dir and removes the temporary version.

So, as long as you don't actually move the file to its final location (which, in your case is the fixture directory), CarrierWave will simply keep using the cached file path. If you break CarrierWave::Mount::Mounter#store! in the test helper, you'll make sure nothing ever actually gets stored while running your tests:

# test/test_helper.rb

ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'

class CarrierWave::Mount::Mounter
  def store!
    # Not storing uploads in the tests
  end
end

class ActiveSupport::TestCase
  include ActionDispatch::TestProcess

  fixtures :all

  CarrierWave.root = Rails.root.join('test/fixtures/files')

  def after_teardown
    super
    CarrierWave.clean_cached_files!(0)
  end
end

Running the tests again, you'll see both tests still pass. The first test loads the file from the fixture directory we created, and the second one uploads a new file to test/fixtures/files/uploads/tmp, which is a path you can easily .gitignore. Also, there's an after_teardown to clean up cached files. We're passing a 0 because CarrierWave defaults to cleaning files that are at least one day old, and we want to remove everything all the time.

I've created a demo Rails project (diff), so you can play around with this yourself. Also, I've submitted a patch to CarrierWave to add a :cache_only option, which would save you some monkey-patching. It's merged in, but it hasn't been released yet, so you'll have to use the edge version of CarrierWave if you want to try it.

If you tried this approach in your project and have anything to add, please let me know!