Skip to content

Commit

Permalink
Add per model automatically_define_enum_traits option
Browse files Browse the repository at this point in the history
Currently you can only specify whether to automatically define enum traits at
a global level, through `FactoryBot.automatically_define_enum_traits`.
This means that an entire codebase has to either opt-in, or opt-out
from automatically defining enum traits. If you are in a large,
established codebase with lots of enum's, this is quite hard to change
globally when you find that automatically defining them doesn't fit for
your new use case.

If we could override this at a per-factory level, we could allow
individual factories to override the global setting where appropriate,
in order to do customise them where necessary.

Given `FactoryBot.automatically_define_enum_traits` being set to `true`,
and a model called `Task` with the following enum definition:

```
class Task
  enum :confirmed_by, [:user, :organisation], prefix: true
end
```

You would be able to override disable this on a per-factory basis like so:

```
FactoryBot.define do
  factory :task, automatically_define_enum_traits: false do
    confirmed_by { :user }

    trait :org_confirmed do
      confirmed_by { :organisation }
    end
  end
end
```

If `FactoryBot.automatically_define_enum_traits` was instead set to
`false`, then the same model with a factory override set to `false` you
would end up with the following automatic traits:

```
FactoryBot.define do
  factory :task, automatically_define_enum_traits: true do
    # The :user and :organisation traits below would be automatically
    # defined in the following way:
    #
    # trait :user do
    #   confirmed_by { :user }
    # end

    # trait :organisation do
    #   confirmed_by { :organisation }
    # end
  end
end
```

Fixes: thoughtbot#1597

Co-Authored-By: Julia Chan <julia.chan@freeagent.com>
  • Loading branch information
mikespokefire and juliachn committed Nov 7, 2023
1 parent 9b9b24f commit bfcf29d
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 7 deletions.
19 changes: 19 additions & 0 deletions docs/src/traits/enum.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ FactoryBot.define do
end
```

If you want to override `FactoryBot.automatically_define_enum_traits` on a
per-model basis, you can use an additional attribute on your factory:

```rb
FactoryBot.define do
factory :task, automatically_define_enum_traits: false do
status { :queued }

trait :in_progress do
status { :started }
end

trait :complete do
status {:finished }
end
end
end
```

It is also possible to use this feature for other enumerable values, not
specifically tied to Active Record enum attributes.

Expand Down
12 changes: 9 additions & 3 deletions lib/factory_bot/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module FactoryBot
class Definition
attr_reader :defined_traits, :declarations, :name, :registered_enums

def initialize(name, base_traits = [])
def initialize(name, base_traits = [], automatically_define_enum_traits = nil)
@name = name
@declarations = DeclarationList.new(name)
@callbacks = []
Expand All @@ -15,6 +15,7 @@ def initialize(name, base_traits = [])
@constructor = nil
@attributes = nil
@compiled = false
@automatically_define_enum_traits = automatically_define_enum_traits
@expanded_enum_traits = false
end

Expand Down Expand Up @@ -195,8 +196,13 @@ def automatically_register_defined_enums(klass)
end

def automatically_register_defined_enums?(klass)
FactoryBot.automatically_define_enum_traits &&
klass.respond_to?(:defined_enums)
automatically_define_enum_traits = if @automatically_define_enum_traits.nil?
FactoryBot.automatically_define_enum_traits
else
@automatically_define_enum_traits
end

automatically_define_enum_traits && klass.respond_to?(:defined_enums)
end
end
end
4 changes: 2 additions & 2 deletions lib/factory_bot/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def initialize(name, options = {})
@parent = options[:parent]
@aliases = options[:aliases] || []
@class_name = options[:class]
@definition = Definition.new(@name, options[:traits] || [])
@definition = Definition.new(@name, options[:traits] || [], options[:automatically_define_enum_traits])
@compiled = false
end

Expand Down Expand Up @@ -140,7 +140,7 @@ def compiled_constructor
private

def assert_valid_options(options)
options.assert_valid_keys(:class, :parent, :aliases, :traits)
options.assert_valid_keys(:class, :parent, :aliases, :traits, :automatically_define_enum_traits)
end

def parent
Expand Down
66 changes: 64 additions & 2 deletions spec/acceptance/enum_traits_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
describe "enum traits" do
context "when automatically_define_enum_traits is true" do
context "when FactoryBot.automatically_define_enum_traits is true" do
it "builds traits automatically for model enum field" do
define_model("Task", status: :integer) do
enum status: {queued: 0, started: 1, finished: 2}
Expand Down Expand Up @@ -113,9 +113,49 @@ def each(&block)
expect(task.status).to eq(trait_name)
end
end

context "when the factory specifies automatically_define_enum_traits as false" do
it "raises an error for undefined traits" do
define_model("Task", status: :integer) do
enum status: {queued: 0, started: 1, finished: 2}
end

FactoryBot.define do
factory :task, automatically_define_enum_traits: false
end

Task.statuses.each_key do |trait_name|
expect { FactoryBot.build(:task, trait_name) }.to raise_error(
KeyError, "Trait not registered: \"#{trait_name}\""
)
end

Task.reset_column_information
end

it "builds traits for each enumerated value when traits_for_enum are specified" do
define_model("Task", status: :integer) do
enum status: {queued: 0, started: 1, finished: 2}
end

FactoryBot.define do
factory :task, automatically_define_enum_traits: false do
traits_for_enum(:status)
end
end

Task.statuses.each_key do |trait_name|
task = FactoryBot.build(:task, trait_name)

expect(task.status).to eq(trait_name)
end

Task.reset_column_information
end
end
end

context "when automatically_define_enum_traits is false" do
context "when FactoryBot.automatically_define_enum_traits is false" do
it "raises an error for undefined traits" do
with_temporary_assignment(FactoryBot, :automatically_define_enum_traits, false) do
define_model("Task", status: :integer) do
Expand Down Expand Up @@ -157,5 +197,27 @@ def each(&block)
Task.reset_column_information
end
end

context "when the factory specifies automatically_define_enum_traits as true" do
it "builds traits automatically for model enum field" do
with_temporary_assignment(FactoryBot, :automatically_define_enum_traits, false) do
define_model("Task", status: :integer) do
enum status: {queued: 0, started: 1, finished: 2}
end

FactoryBot.define do
factory :task, automatically_define_enum_traits: true
end

Task.statuses.each_key do |trait_name|
task = FactoryBot.build(:task, trait_name)

expect(task.status).to eq(trait_name)
end

Task.reset_column_information
end
end
end
end
end

0 comments on commit bfcf29d

Please sign in to comment.