How to add Active Storage attachement as a Factory Bot (or Factory Girl) trait.

technology used in the example: Rails 5.2.0, Ruby 2.5, Factory Bot 4.10, RSpec 3.7

# app/models/account.rb
class Account < ActiveRecord::Base
  has_attached :image
end

# spec/rails_helper.rb
FactoryBot::SyntaxRunner.class_eval do
  include ActionDispatch::TestProcess
end

# spec/factories/accounts.rb
FactoryBot.define do
  factory :account do
    name 'Tomas'

    trait :with_avatar do
      avatar { fixture_file_upload(Rails.root.join('spec', 'support', 'assets', 'test-image.png'), 'image/png') }
    end
  end
end

make sure you have test image to spec/support/assets/test-image.png

Now you can do:

# spec/models/account_spec.rb
let(:account) { build :account, :with_avatar)
let(:account) { build_stubbed :account, :with_avatar)
let(:account) { create :account, :with_avatar)

Alternative way:

# spec/factories/accounts.rb
FactoryBot.define do
  factory :account do
    name 'Tomas'

    trait :with_avatar do
      after :create do |account|
        file_path = Rails.root.join('spec', 'support', 'assets', 'test-image.png')
        file = fixture_file_upload(file_path, 'image/png')
        account.avatar.attach(file)
      end
    end
  end
end

# spec/models/account_spec.rb
let(:account) { create :account, :with_avatar)

didn’t try it but teoretically you can use this approach to attach has_many_attached

Using Test Helpers

My favorit approach is to create helper module that would extend everything required for attaching images:

# spec/support/files_test_helper.rb
module FilesTestHelper
  extend self
  extend ActionDispatch::TestProcess

  def png_name; 'test-image.png' end
  def png; upload(png_name, 'image/png') end

  def jpg_name; 'test-image.jpg' end
  def jpg; upload(jpg_name, 'image/jpg') end

  def tiff_name; 'test-image.tiff' end
  def tiff; upload(tiff_name, 'image/tiff') end

  def pdf_name; 'test.pdf' end
  def pdf; upload(pdf_name, 'application/pdf') end

  private

  def upload(name, type)
    file_path = Rails.root.join('spec', 'support', 'assets', name)
    fixture_file_upload(file_path, type)
  end
end
# spec/factories/accounts.rb
FactoryBot.define do
  factory :account do
    name 'Tomas'

    trait :with_avatar do
      avatar { FilesTestHelper.png }
    end
  end
end

This way I don’t have to polute Factory Bot with: FactoryBot::SyntaxRunner.class_eval { include ActionDispatch::TestProcess } making the debugging easier for junior developers.

And I’m also able to reuse the test helper e.g. in controler specs when testing upload:

RSpec.describe V3::AccountsController, type: :controller do
  describe 'POST create' do
    let(:avatar) { FilesTestHelper.png }

    def trigger do
      post :create, params: { avatar: avatar, name: 'Zdenka' }
    end

    it 'should upload the file' do
      expect { trigger }.to change{ ActiveStorage::Attachment.count }.by(1)
    end

    it 'should create the account' do
      expect { trigger }.to change{ Account.count }.by(1)
      account = Account.last
      expect(account.avatar).to be_attached
      expect(account.avatar.filename).to eq FilesTestHelper.png_name
      expect(account.name).to eq 'Zdenka'
     end
	end
end

Discussion: