Skip to content

Commit

Permalink
Support file_fixture in Factory definitions
Browse files Browse the repository at this point in the history
Related to [factory_bot#1282][]

[rails/rails#45606][] has been merged and is likely to be released as
part of Rails 7.1.

With that addition, the path toward resolving [factory_bot#1282][]
becomes more clear. If factories can pass along [Pathname][] instances
to attachment attributes, Active Support will handle the rest.

Instances of `ActiveSupport::TestCase` provide a [file_fixture][] helper
to construct a `Pathname` instance based on the path defined by
`ActiveSupport::TestCase.file_fixture_path` (relative to the Rails root
directory).

[factory_bot#1282]: thoughtbot/factory_bot#1282 (comment)
[rails/rails#45606]: rails/rails#45606
[Pathname]: https://docs.ruby-lang.org/en/master/Pathname.html
[file_fixture]: https://api.rubyonrails.org/classes/ActiveSupport/Testing/FileFixtures.html#method-i-file_fixture
  • Loading branch information
seanpdoyle committed Jan 10, 2025
1 parent 346e3c7 commit b9df074
Show file tree
Hide file tree
Showing 17 changed files with 269 additions and 68 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ using an empty array:
config.factory_bot.definition_file_paths = []
```

### File Fixture Support

Factories have access to [ActiveSupport::Testing::FileFixtures#file_fixture][]
helper to read files from tests.

To disable file fixture support, set `file_fixture_support = false`:

```rb
config.factory_bot.file_fixture_support = false
```

[ActiveSupport::Testing::FileFixtures#file_fixture]: https://api.rubyonrails.org/classes/ActiveSupport/Testing/FileFixtures.html#method-i-file_fixture

### Generators

Including factory\_bot\_rails in the development group of your Gemfile
Expand Down
5 changes: 4 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "bundler/setup"
require "cucumber/rake/task"
require "rspec/core/rake_task"
require "minitest/test_task"
require "standard/rake"

Bundler::GemHelper.install_tasks name: "factory_bot_rails"
Expand All @@ -12,5 +13,7 @@ end

RSpec::Core::RakeTask.new(:spec)

Minitest::TestTask.create

desc "Run the test suite and standard"
task default: %w[spec cucumber standard]
task default: %w[spec test cucumber standard]
9 changes: 9 additions & 0 deletions lib/factory_bot_rails/file_fixture_support.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module FactoryBotRails
module FileFixtureSupport
def self.included(klass)
klass.cattr_accessor :file_fixture_support

klass.delegate :file_fixture, to: "self.class.file_fixture_support"
end
end
end
18 changes: 18 additions & 0 deletions lib/factory_bot_rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
require "factory_bot_rails/generator"
require "factory_bot_rails/reloader"
require "factory_bot_rails/factory_validator"
require "factory_bot_rails/file_fixture_support"
require "rails"

module FactoryBotRails
class Railtie < Rails::Railtie
config.factory_bot = ActiveSupport::OrderedOptions.new
config.factory_bot.definition_file_paths = FactoryBot.definition_file_paths
config.factory_bot.validator = FactoryBotRails::FactoryValidator.new
config.factory_bot.file_fixture_support = true

initializer "factory_bot.set_fixture_replacement" do
Generator.new(config).run
Expand All @@ -20,6 +22,22 @@ class Railtie < Rails::Railtie
FactoryBot.definition_file_paths = definition_file_paths
end

config.after_initialize do
if config.factory_bot.file_fixture_support
FactoryBot::SyntaxRunner.include FactoryBotRails::FileFixtureSupport

ActiveSupport.on_load :active_support_test_case do
setup { FactoryBot::SyntaxRunner.file_fixture_support = self }
end

if defined?(RSpec) && RSpec.respond_to?(:configure)
RSpec.configure do |config|
config.before { FactoryBot::SyntaxRunner.file_fixture_support = self }
end
end
end
end

config.after_initialize do |app|
FactoryBot.find_definitions
Reloader.new(app).run
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types

create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum

if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end

t.index [ :key ], unique: true
end

create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type

if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end

t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end

create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false

t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end

private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[ primary_key_type, foreign_key_type ]
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
def up
return unless table_exists?(:active_storage_blobs)

unless column_exists?(:active_storage_blobs, :service_name)
add_column :active_storage_blobs, :service_name, :string

if configured_service = ActiveStorage::Blob.service.name
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
end

change_column :active_storage_blobs, :service_name, :string, null: false
end
end

def down
return unless table_exists?(:active_storage_blobs)

remove_column :active_storage_blobs, :service_name
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
def change
return unless table_exists?(:active_storage_blobs)

# Use Active Record's configured type for primary key
create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t|
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
t.string :variation_digest, null: false

t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end

private
def primary_key_type
config = Rails.configuration.generators
config.options[config.orm][:primary_key_type] || :primary_key
end

def blobs_primary_key_type
pkey_name = connection.primary_key(:active_storage_blobs)
pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name }
pkey_column.bigint? ? :bigint : pkey_column.type
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
def change
return unless table_exists?(:active_storage_blobs)

change_column_null(:active_storage_blobs, :checksum, true)
end
end
40 changes: 40 additions & 0 deletions spec/factory_bot_rails/factory_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require "spec_helper"

describe "factory extensions" do
describe "#file_fixture" do
it "delegates to the test harness" do
FactoryBot.define do
factory :upload, class: Struct.new(:filename) do
filename { file_fixture("file.txt") }
end
end

upload = FactoryBot.build(:upload)

expect(Pathname(upload.filename)).to eq(file_fixture("file.txt"))
end

it "uploads an ActiveStorage::Blob" do
FactoryBot.define do
factory :active_storage_blob, class: ActiveStorage::Blob do
filename { pathname.basename }

transient do
pathname { file_fixture("file.txt") }
end

after :build do |model, factory|
model.upload factory.pathname.open
end
end
end

blob = FactoryBot.create(:active_storage_blob)

expect(blob.filename.to_s).to eq("file.txt")
expect(blob.download).to eq(file_fixture("file.txt").read)
end
end
end
13 changes: 13 additions & 0 deletions spec/fake_app.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# frozen_string_literal: true

ENV["DATABASE_URL"] = "sqlite3::memory:"

require "active_record/railtie"
require "active_storage/engine"

module Dummy
class Application < Rails::Application
config.eager_load = false
Expand All @@ -8,6 +13,14 @@ class Application < Rails::Application
if Rails.gem_version >= Gem::Version.new("7.1")
config.active_support.cache_format_version = 7
end

config.active_storage.service = :local
config.active_storage.service_configurations = {
local: {
root: root.join("tmp/storage"),
service: "Disk"
}
}
end
end

Expand Down
Empty file added spec/fixtures/files/file.txt
Empty file.
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

require "factory_bot_rails"
require "fake_app"
require "rspec/rails"

Dir["spec/support/**/*.rb"].each { |f| require File.expand_path(f) }

Expand Down
10 changes: 10 additions & 0 deletions spec/support/active_record/migrations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

Rails.root.glob("../dummy/db/migrate/*.rb").each do |file|
require file

if file.read =~ /\Aclass (\w+)/ && (migration = Regexp.last_match(1).safe_constantize)
migration.verbose = false
migration.migrate :up
end
end
67 changes: 0 additions & 67 deletions spec/support/macros/define_constant.rb

This file was deleted.

40 changes: 40 additions & 0 deletions test/factory_bot_rails/factory_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require "test_helper"

class FactoryBotRails::FactoryTest < ActiveSupport::TestCase
self.file_fixture_path = "test/fixtures/files"

test "delegates #file_fixture to the test harness" do
FactoryBot.define do
factory :upload, class: Struct.new(:filename) do
filename { file_fixture("file.txt") }
end
end

upload = FactoryBot.build(:upload)

assert_equal file_fixture("file.txt"), upload.filename
end

test "uploads an ActiveStorage::Blob" do
FactoryBot.define do
factory :active_storage_blob, class: ActiveStorage::Blob do
filename { pathname.basename }

transient do
pathname { file_fixture("file.txt") }
end

after :build do |model, factory|
model.upload factory.pathname.open
end
end
end

blob = FactoryBot.create(:active_storage_blob)

assert_equal "file.txt", blob.filename.to_s
assert_equal file_fixture("file.txt").read, blob.download
end
end
Empty file added test/fixtures/files/file.txt
Empty file.
Loading

0 comments on commit b9df074

Please sign in to comment.