Testing with Shrine
The goal of this guide is to provide some useful tips for testing file attachments implemented with Shrine in your application.
Callbacks
When you first try to test file attachments, you might experience that files are not being promoted to permanent storage. This is because your tests are likely setup to be wrapped inside database transactions, and that doesn't work with Shrine callbacks.
Specifically, Shrine uses "after commit" callbacks for promoting and deleting attached files. This means that if your tests are wrapped inside transactions, those Shrine actions will happen only after those transactions commit, which happens only after the test has already finished.
# Promoting will happen only after the test transaction commits
it "can attach images" do
photo = Photo.create(image: file)
photo.image.storage_key #=> :cache (we expected it to be promoted to permanent storage)
end
For file attachments to properly work, you'll need to disable transactions for those tests. For Rails apps you can tell Rails not to use transactions, and instead use libraries like DatabaseCleaner which allow you to use table truncation or deletion strategies instead of transactions.
RSpec.configure do |config|
config.use_transactional_fixtures = false
end
Storage
If you're using FileSystem storage and your tests run in a single process,
you can switch to Shrine::Storage::Memory
, which is both faster and doesn't
require you to clean up anything between tests.
require "shrine/storage/memory"
Shrine.storages = {
cache: Shrine::Storage::Memory.new,
store: Shrine::Storage::Memory.new,
}
If you're using AWS S3 storage, you can use MinIO (explained below) instead of S3, both in test and development environment. Alternatively, you can stub aws-sdk-s3 requests in tests.
MinIO
MinIO is an open source object storage server with AWS S3 compatible API which you can run locally. The advantage of using MinIO for your development and test environments is that all AWS S3 functionality should still continue to work, including direct uploads, so you don't need to update your code.
If you're on a Mac you can install it with Homebrew:
$ brew install minio/stable/minio
Afterwards you can start the MinIO server and give it a directory where it will store the data:
$ minio server data/
This command will print out the credentials for the running MinIO server, as
well as a link to the MinIO web interface. Follow that link and create a new
bucket. Once you've done that, you can configure Shrine::Storage::S3
to use
your MinIO server:
Shrine::Storage::S3.new(
access_key_id: "<MINIO_ACCESS_KEY>", # "AccessKey" value
secret_access_key: "<MINIO_SECRET_KEY>", # "SecretKey" value
endpoint: "<MINIO_ENDPOINT>", # "Endpoint" value
bucket: "<MINIO_BUCKET>", # name of the bucket you created
region: "us-east-1",
force_path_style: true,
)
The :endpoint
option will make aws-sdk-s3 point all URLs to your MinIO server
(instead of s3.amazonaws.com
), and :force_path_style
tells it not to use
subdomains when generating URLs.
Test data
We want to keep our tests fast, so when we're setting up files for tests, we want to avoid expensive operations such as file processing and metadata extraction.
We can create a helper method that will create attached file data for us, and use that with our factories/fixtures.
module TestData
module_function
def image_data
attacher = Shrine::Attacher.new
attacher.set(uploaded_image)
# if you're processing derivatives
attacher.set_derivatives(
large: uploaded_image,
medium: uploaded_image,
small: uploaded_image,
)
attacher.column_data # or attacher.data in case of postgres jsonb column
end
def uploaded_image
file = File.open("test/files/image.jpg", binmode: true)
# for performance we skip metadata extraction and assign test metadata
uploaded_file = Shrine.upload(file, :store, metadata: false)
uploaded_file.metadata.merge!(
"size" => File.size(file.path),
"mime_type" => "image/jpeg",
"filename" => "test.jpg",
)
uploaded_file
end
end
- FactoryBot
- Rails YAML fixtures
factory :photo do
image_data { TestData.image_data }
end
photo:
image_data: <%= TestData.image_data %>
Unit tests
For testing attachment in your unit tests, you can assign plain File
objects:
RSpec.describe ImageUploader do
let(:image) { photo.image }
let(:derivatives) { photo.image_derivatives }
let(:photo) { Photo.create(image: File.open("test/files/image.png", "rb")) }
it "extracts metadata" do
expect(image.mime_type).to eq("image/png")
expect(image.extension).to eq("png")
expect(image.size).to be_instance_of(Integer)
expect(image.width).to be_instance_of(Integer)
expect(image.height).to be_instance_of(Integer)
end
it "generates derivatives" do
expect(derivatives[:small]).to be_kind_of(Shrine::UploadedFile)
expect(derivatives[:medium]).to be_kind_of(Shrine::UploadedFile)
expect(derivatives[:large]).to be_kind_of(Shrine::UploadedFile)
end
end
Acceptance tests
In acceptance tests you're testing your app end-to-end, and you likely want to also test file attachments here. Here are examples for some common use cases:
- Capybara
- rack-test
attach_file("#image-field", "test/files/image.jpg")
post "/photos", photo: {
image: Rack::Test::UploadedFile.new("test/files/image.jpg", "image/jpeg")
}
If you want to test requests with cached attachment data, you can do so as follows:
cached_file = Shrine.upload(file, :cache)
post "/photos", photo: { image: cached_file.to_json }
Background jobs
If you're using background jobs with Shrine, you probably want to make them synchronous in tests. See your backgrounding library docs for how to make jobs synchronous.
- Active Job
- Sidekiq
- SuckerPunch
ActiveJob::Base.queue_adapter = :inline
require "sidekiq/testing"
Sidekiq::Testing.inline!
require "sucker_punch/testing/inline"
Processing
If you're testing your attachment flow which includes processing derivatives, you might want to disable the processing for certain tests. You can do this by temporarily overriding the processor:
module TestMode
module_function
def disable_processing(attacher, processor_name = :default)
attacher.class.instance_exec do
original_processor = derivatives_processor
derivatives_processor(processor_name) { Hash.new }
yield
derivatives_processor(processor_name, &original_processor)
end
end
end
TestMode.disable_processing(Photo.image_attacher) do
photo = Photo.new
photo.file = File.open("test/files/image.png", "rb")
photo.save
end
Testing direct upload
If you'd like to unit-test direct upload on the server side, you can
emulate it by uploading a file to cache
and then assigning it to the record.
cached_file = Shrine.upload(some_file, :cache)
record.attachment = cached_file.to_json