· documentation · 2 min read
Dealing with Direct Upload Attachments in Tests
The process of directly uploading an attachment means that you are attaching a blob using the blob's `signed_id`. Read on to see how to access a blob's signed_id before the blob is attached to your ActiveRecord model instance in an RSpec suite.
I was recently building a feature for the Agency of Learning where I was working with ActiveSupport to add an attachment to a model. In my implementation, I used direct uploads to first upload the attachment to s3, then submit the form with the signed ID.
Direct uploads are totally magic. You pass a hashmap containing the attachment name as the key, and the signed_id
as the value and ActiveSupport handles the attachment for you.
For example, say I have a Resume model with an attached resume:
# app/models/resume.rb
class Resume < ApplicationRecord
belongs_to :user
has_one_attached :resume
validates :resume, content_type: ['application/pdf']
end
Attaching and persisting the blob to an instance of Resume
is straightforward.
params = { resume: uploaded_resume_signed_id }
# let's assume I have a User in my database, and a `User` `has_many :resumes`
User.last.resumes.create!(params)
When it came time to test the service object I built, I wanted to mimic the contract that my method calls would be expecting as close as possible.
In our testing stack, we’re not leaning heavily on minitest and fixtures. Instead, we’re using FactoryBot and Rspec. This means that the fixtures approach to testing these attachments prescribed in the official Ruby on Rails guides don’t quite fit the way we write our tests.
As a result, I thought I’d try to figure out a quick/simple way to generate the signed ID for the valid Blob that the ActiveRecord model is expecting. One solution? Leveraging the ActiveStorage::Blob.create_and_upload!
method.
Here’s a snippet of the spec I wrote:
# resumes_update_spec.rb
let(:user) { create(:user) }
let(:uploaded_resume_signed_id) do
ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/fixtures/bob_resume.pdf').open,
filename: 'bob_resume.pdf',
content_type: 'application/pdf'
).signed_id
end
let(:params) do
ActionController::Parameters.new({
profile: {
resume: uploaded_resume_signed_id,
}
}).require(:profile).permit(:resume)
end
it 'associates the resume with the user' do
expect { Resumes::Update.new(user:, params:).call! }.to change {
user.resumes.count
}.by(1)
end
Hopefully this post helps save you some time if you find yourself in a similar situation in the future!