factory_bot 5.1.2 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +52 -13
  3. data/GETTING_STARTED.md +441 -66
  4. data/NEWS.md +26 -1
  5. data/README.md +5 -11
  6. data/lib/factory_bot.rb +20 -54
  7. data/lib/factory_bot/aliases.rb +3 -3
  8. data/lib/factory_bot/attribute/association.rb +2 -2
  9. data/lib/factory_bot/attribute/dynamic.rb +1 -1
  10. data/lib/factory_bot/attribute_assigner.rb +9 -10
  11. data/lib/factory_bot/attribute_list.rb +1 -1
  12. data/lib/factory_bot/callback.rb +2 -10
  13. data/lib/factory_bot/configuration.rb +7 -7
  14. data/lib/factory_bot/declaration.rb +1 -1
  15. data/lib/factory_bot/declaration/association.rb +23 -6
  16. data/lib/factory_bot/declaration_list.rb +2 -2
  17. data/lib/factory_bot/decorator.rb +18 -6
  18. data/lib/factory_bot/decorator/invocation_tracker.rb +10 -3
  19. data/lib/factory_bot/definition.rb +46 -15
  20. data/lib/factory_bot/definition_hierarchy.rb +1 -11
  21. data/lib/factory_bot/definition_proxy.rb +64 -6
  22. data/lib/factory_bot/enum.rb +27 -0
  23. data/lib/factory_bot/evaluator.rb +19 -11
  24. data/lib/factory_bot/evaluator_class_definer.rb +1 -1
  25. data/lib/factory_bot/factory.rb +12 -12
  26. data/lib/factory_bot/factory_runner.rb +3 -3
  27. data/lib/factory_bot/find_definitions.rb +1 -1
  28. data/lib/factory_bot/internal.rb +18 -29
  29. data/lib/factory_bot/linter.rb +8 -12
  30. data/lib/factory_bot/null_factory.rb +11 -5
  31. data/lib/factory_bot/null_object.rb +2 -6
  32. data/lib/factory_bot/registry.rb +2 -2
  33. data/lib/factory_bot/reload.rb +0 -1
  34. data/lib/factory_bot/sequence.rb +5 -5
  35. data/lib/factory_bot/strategy/null.rb +4 -2
  36. data/lib/factory_bot/strategy/stub.rb +6 -2
  37. data/lib/factory_bot/strategy_syntax_method_registrar.rb +12 -1
  38. data/lib/factory_bot/syntax/default.rb +7 -19
  39. data/lib/factory_bot/trait.rb +1 -1
  40. data/lib/factory_bot/version.rb +1 -1
  41. metadata +7 -34
@@ -6,9 +6,16 @@ module FactoryBot
6
6
  @invoked_methods = []
7
7
  end
8
8
 
9
- def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing
10
- @invoked_methods << name
11
- super
9
+ if ::Gem::Version.new(::RUBY_VERSION) >= ::Gem::Version.new("2.7")
10
+ def method_missing(name, *args, **kwargs, &block) # rubocop:disable Style/MissingRespondToMissing
11
+ @invoked_methods << name
12
+ super
13
+ end
14
+ else
15
+ def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing
16
+ @invoked_methods << name
17
+ super
18
+ end
12
19
  end
13
20
 
14
21
  def __invoked_methods__
@@ -1,19 +1,21 @@
1
1
  module FactoryBot
2
2
  # @api private
3
3
  class Definition
4
- attr_reader :defined_traits, :declarations, :name
4
+ attr_reader :defined_traits, :declarations, :name, :registered_enums
5
5
 
6
6
  def initialize(name, base_traits = [])
7
- @name = name
8
- @declarations = DeclarationList.new(name)
9
- @callbacks = []
10
- @defined_traits = Set.new
11
- @to_create = nil
12
- @base_traits = base_traits
7
+ @name = name
8
+ @declarations = DeclarationList.new(name)
9
+ @callbacks = []
10
+ @defined_traits = Set.new
11
+ @registered_enums = []
12
+ @to_create = nil
13
+ @base_traits = base_traits
13
14
  @additional_traits = []
14
- @constructor = nil
15
- @attributes = nil
16
- @compiled = false
15
+ @constructor = nil
16
+ @attributes = nil
17
+ @compiled = false
18
+ @expanded_enum_traits = false
17
19
  end
18
20
 
19
21
  delegate :declare_attribute, to: :declarations
@@ -43,12 +45,14 @@ module FactoryBot
43
45
  aggregate_from_traits_and_self(:callbacks) { @callbacks }
44
46
  end
45
47
 
46
- def compile
48
+ def compile(klass = nil)
47
49
  unless @compiled
50
+ expand_enum_traits(klass) unless klass.nil?
51
+
48
52
  declarations.attributes
49
53
 
50
54
  defined_traits.each do |defined_trait|
51
- base_traits.each { |bt| bt.define_trait defined_trait }
55
+ base_traits.each { |bt| bt.define_trait defined_trait }
52
56
  additional_traits.each { |at| at.define_trait defined_trait }
53
57
  end
54
58
 
@@ -81,6 +85,10 @@ module FactoryBot
81
85
  @defined_traits.add(trait)
82
86
  end
83
87
 
88
+ def register_enum(enum)
89
+ @registered_enums << enum
90
+ end
91
+
84
92
  def define_constructor(&block)
85
93
  @constructor = block
86
94
  end
@@ -95,7 +103,6 @@ module FactoryBot
95
103
 
96
104
  def callback(*names, &block)
97
105
  names.each do |name|
98
- FactoryBot::Internal.register_callback(name)
99
106
  add_callback(Callback.new(name, block))
100
107
  end
101
108
  end
@@ -122,7 +129,7 @@ module FactoryBot
122
129
  def initialize_copy(source)
123
130
  super
124
131
  @attributes = nil
125
- @compiled = false
132
+ @compiled = false
126
133
  @defined_traits_by_name = nil
127
134
  end
128
135
 
@@ -132,8 +139,32 @@ module FactoryBot
132
139
  [
133
140
  base_traits.map(&method_name),
134
141
  instance_exec(&block),
135
- additional_traits.map(&method_name),
142
+ additional_traits.map(&method_name)
136
143
  ].flatten.compact
137
144
  end
145
+
146
+ def expand_enum_traits(klass)
147
+ return if @expanded_enum_traits
148
+
149
+ if automatically_register_defined_enums?(klass)
150
+ automatically_register_defined_enums(klass)
151
+ end
152
+
153
+ registered_enums.each do |enum|
154
+ traits = enum.build_traits(klass)
155
+ traits.each { |trait| define_trait(trait) }
156
+ end
157
+
158
+ @expanded_enum_traits = true
159
+ end
160
+
161
+ def automatically_register_defined_enums(klass)
162
+ klass.defined_enums.each_key { |name| register_enum(Enum.new(name)) }
163
+ end
164
+
165
+ def automatically_register_defined_enums?(klass)
166
+ FactoryBot.automatically_define_enum_traits &&
167
+ klass.respond_to?(:defined_enums)
168
+ end
138
169
  end
139
170
  end
@@ -1,16 +1,6 @@
1
1
  module FactoryBot
2
2
  class DefinitionHierarchy
3
- def callbacks
4
- FactoryBot.callbacks
5
- end
6
-
7
- def constructor
8
- FactoryBot.constructor
9
- end
10
-
11
- def to_create
12
- FactoryBot.to_create
13
- end
3
+ delegate :callbacks, :constructor, :to_create, to: Internal
14
4
 
15
5
  def self.build_from_definition(definition)
16
6
  build_to_create(&definition.to_create)
@@ -1,6 +1,6 @@
1
1
  module FactoryBot
2
2
  class DefinitionProxy
3
- UNPROXIED_METHODS = %w(
3
+ UNPROXIED_METHODS = %w[
4
4
  __send__
5
5
  __id__
6
6
  nil?
@@ -13,7 +13,7 @@ module FactoryBot
13
13
  raise
14
14
  caller
15
15
  method
16
- ).freeze
16
+ ].freeze
17
17
 
18
18
  (instance_methods + private_instance_methods).each do |method|
19
19
  undef_method(method) unless UNPROXIED_METHODS.include?(method.to_s)
@@ -24,8 +24,8 @@ module FactoryBot
24
24
  attr_reader :child_factories
25
25
 
26
26
  def initialize(definition, ignore = false)
27
- @definition = definition
28
- @ignore = ignore
27
+ @definition = definition
28
+ @ignore = ignore
29
29
  @child_factories = []
30
30
  end
31
31
 
@@ -88,7 +88,7 @@ module FactoryBot
88
88
  # end
89
89
  #
90
90
  # are equivalent.
91
- def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing, Style/MethodMissingSuper, Metrics/LineLength
91
+ def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing, Style/MethodMissingSuper
92
92
  association_options = args.first
93
93
 
94
94
  if association_options.nil?
@@ -152,7 +152,7 @@ module FactoryBot
152
152
  if block_given?
153
153
  raise AssociationDefinitionError.new(
154
154
  "Unexpected block passed to '#{name}' association "\
155
- "in '#{@definition.name}' factory",
155
+ "in '#{@definition.name}' factory"
156
156
  )
157
157
  else
158
158
  declaration = Declaration::Association.new(name, *options)
@@ -176,6 +176,64 @@ module FactoryBot
176
176
  @definition.define_trait(Trait.new(name, &block))
177
177
  end
178
178
 
179
+ # Creates traits for enumerable values.
180
+ #
181
+ # Example:
182
+ # factory :task do
183
+ # traits_for_enum :status, [:started, :finished]
184
+ # end
185
+ #
186
+ # Equivalent to:
187
+ # factory :task do
188
+ # trait :started do
189
+ # status { :started }
190
+ # end
191
+ #
192
+ # trait :finished do
193
+ # status { :finished }
194
+ # end
195
+ # end
196
+ #
197
+ # Example:
198
+ # factory :task do
199
+ # traits_for_enum :status, {started: 1, finished: 2}
200
+ # end
201
+ #
202
+ # Example:
203
+ # class Task
204
+ # def statuses
205
+ # {started: 1, finished: 2}
206
+ # end
207
+ # end
208
+ #
209
+ # factory :task do
210
+ # traits_for_enum :status
211
+ # end
212
+ #
213
+ # Both equivalent to:
214
+ # factory :task do
215
+ # trait :started do
216
+ # status { 1 }
217
+ # end
218
+ #
219
+ # trait :finished do
220
+ # status { 2 }
221
+ # end
222
+ # end
223
+ #
224
+ #
225
+ # Arguments:
226
+ # attribute_name: +Symbol+ or +String+
227
+ # the name of the attribute these traits will set the value of
228
+ # values: +Array+, +Hash+, or other +Enumerable+
229
+ # An array of trait names, or a mapping of trait names to values for
230
+ # those traits. When this argument is not provided, factory_bot will
231
+ # attempt to get the values by calling the pluralized `attribute_name`
232
+ # class method.
233
+ def traits_for_enum(attribute_name, values = nil)
234
+ @definition.register_enum(Enum.new(attribute_name, values))
235
+ end
236
+
179
237
  def initialize_with(&block)
180
238
  @definition.define_constructor(&block)
181
239
  end
@@ -0,0 +1,27 @@
1
+ module FactoryBot
2
+ # @api private
3
+ class Enum
4
+ def initialize(attribute_name, values = nil)
5
+ @attribute_name = attribute_name
6
+ @values = values
7
+ end
8
+
9
+ def build_traits(klass)
10
+ enum_values(klass).map do |trait_name, value|
11
+ build_trait(trait_name, @attribute_name, value || trait_name)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def enum_values(klass)
18
+ @values || klass.send(@attribute_name.to_s.pluralize)
19
+ end
20
+
21
+ def build_trait(trait_name, attribute_name, value)
22
+ Trait.new(trait_name) do
23
+ add_attribute(attribute_name) { value }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -7,7 +7,7 @@ module FactoryBot
7
7
  class_attribute :attribute_lists
8
8
 
9
9
  private_instance_methods.each do |method|
10
- undef_method(method) unless method =~ /^__|initialize/
10
+ undef_method(method) unless method.match?(/^__|initialize/)
11
11
  end
12
12
 
13
13
  def initialize(build_strategy, overrides = {})
@@ -23,9 +23,9 @@ module FactoryBot
23
23
 
24
24
  def association(factory_name, *traits_and_overrides)
25
25
  overrides = traits_and_overrides.extract_options!
26
- strategy_override = overrides.fetch(:strategy) do
26
+ strategy_override = overrides.fetch(:strategy) {
27
27
  FactoryBot.use_parent_strategy ? @build_strategy.class : :create
28
- end
28
+ }
29
29
 
30
30
  traits_and_overrides += [overrides.except(:strategy)]
31
31
 
@@ -33,15 +33,23 @@ module FactoryBot
33
33
  @build_strategy.association(runner)
34
34
  end
35
35
 
36
- def instance=(object_instance)
37
- @instance = object_instance
38
- end
36
+ attr_accessor :instance
39
37
 
40
- def method_missing(method_name, *args, &block) # rubocop:disable Style/MethodMissingSuper
41
- if @instance.respond_to?(method_name)
42
- @instance.send(method_name, *args, &block)
43
- else
44
- SyntaxRunner.new.send(method_name, *args, &block)
38
+ if ::Gem::Version.new(::RUBY_VERSION) >= ::Gem::Version.new("2.7")
39
+ def method_missing(method_name, *args, **kwargs, &block) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
40
+ if @instance.respond_to?(method_name)
41
+ @instance.send(method_name, *args, **kwargs, &block)
42
+ else
43
+ SyntaxRunner.new.send(method_name, *args, **kwargs, &block)
44
+ end
45
+ end
46
+ else
47
+ def method_missing(method_name, *args, &block) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
48
+ if @instance.respond_to?(method_name)
49
+ @instance.send(method_name, *args, &block)
50
+ else
51
+ SyntaxRunner.new.send(method_name, *args, &block)
52
+ end
45
53
  end
46
54
  end
47
55
 
@@ -3,7 +3,7 @@ module FactoryBot
3
3
  class EvaluatorClassDefiner
4
4
  def initialize(attributes, parent_class)
5
5
  @parent_class = parent_class
6
- @attributes = attributes
6
+ @attributes = attributes
7
7
 
8
8
  attributes.each do |attribute|
9
9
  evaluator_class.define_attribute(attribute.name, &attribute.to_proc)
@@ -8,23 +8,23 @@ module FactoryBot
8
8
 
9
9
  def initialize(name, options = {})
10
10
  assert_valid_options(options)
11
- @name = name.respond_to?(:to_sym) ? name.to_sym : name.to_s.underscore.to_sym
12
- @parent = options[:parent]
13
- @aliases = options[:aliases] || []
14
- @class_name = options[:class]
15
- @definition = Definition.new(@name, options[:traits] || [])
16
- @compiled = false
11
+ @name = name.respond_to?(:to_sym) ? name.to_sym : name.to_s.underscore.to_sym
12
+ @parent = options[:parent]
13
+ @aliases = options[:aliases] || []
14
+ @class_name = options[:class]
15
+ @definition = Definition.new(@name, options[:traits] || [])
16
+ @compiled = false
17
17
  end
18
18
 
19
19
  delegate :add_callback, :declare_attribute, :to_create, :define_trait, :constructor,
20
- :defined_traits, :inherit_traits, :append_traits, to: :@definition
20
+ :defined_traits, :inherit_traits, :append_traits, to: :@definition
21
21
 
22
22
  def build_class
23
23
  @build_class ||= if class_name.is_a? Class
24
- class_name
25
- else
26
- class_name.to_s.camelize.constantize
27
- end
24
+ class_name
25
+ else
26
+ class_name.to_s.camelize.constantize
27
+ end
28
28
  end
29
29
 
30
30
  def run(build_strategy, overrides, &block)
@@ -84,7 +84,7 @@ module FactoryBot
84
84
  unless @compiled
85
85
  parent.compile
86
86
  parent.defined_traits.each { |trait| define_trait(trait) }
87
- @definition.compile
87
+ @definition.compile(build_class)
88
88
  build_hierarchy
89
89
  @compiled = true
90
90
  end
@@ -1,11 +1,11 @@
1
1
  module FactoryBot
2
2
  class FactoryRunner
3
3
  def initialize(name, strategy, traits_and_overrides)
4
- @name = name
4
+ @name = name
5
5
  @strategy = strategy
6
6
 
7
7
  @overrides = traits_and_overrides.extract_options!
8
- @traits = traits_and_overrides
8
+ @traits = traits_and_overrides
9
9
  end
10
10
 
11
11
  def run(runner_strategy = @strategy, &block)
@@ -22,7 +22,7 @@ module FactoryBot
22
22
  strategy: runner_strategy,
23
23
  traits: @traits,
24
24
  overrides: @overrides,
25
- factory: factory,
25
+ factory: factory
26
26
  }
27
27
 
28
28
  ActiveSupport::Notifications.instrument("factory_bot.run_factory", instrumentation_payload) do
@@ -7,7 +7,7 @@ module FactoryBot
7
7
  attr_accessor :definition_file_paths
8
8
  end
9
9
 
10
- self.definition_file_paths = %w(factories test/factories spec/factories)
10
+ self.definition_file_paths = %w[factories test/factories spec/factories]
11
11
 
12
12
  def self.find_definitions
13
13
  absolute_definition_file_paths = definition_file_paths.map { |path| File.expand_path(path) }
@@ -1,26 +1,20 @@
1
1
  module FactoryBot
2
2
  # @api private
3
3
  module Internal
4
- DEFAULT_STRATEGIES = {
5
- build: FactoryBot::Strategy::Build,
6
- create: FactoryBot::Strategy::Create,
7
- attributes_for: FactoryBot::Strategy::AttributesFor,
8
- build_stubbed: FactoryBot::Strategy::Stub,
9
- null: FactoryBot::Strategy::Null,
10
- }.freeze
11
-
12
- DEFAULT_CALLBACKS = [
13
- :after_create, :after_build, :after_stub, :after_create
14
- ].freeze
15
-
16
4
  class << self
17
- delegate :callback_names,
18
- :factories,
19
- :inline_sequences,
20
- :sequences,
21
- :strategies,
22
- :traits,
23
- to: :configuration
5
+ delegate :after,
6
+ :before,
7
+ :callbacks,
8
+ :constructor,
9
+ :factories,
10
+ :initialize_with,
11
+ :inline_sequences,
12
+ :sequences,
13
+ :skip_create,
14
+ :strategies,
15
+ :to_create,
16
+ :traits,
17
+ to: :configuration
24
18
 
25
19
  def configuration
26
20
  @configuration ||= Configuration.new
@@ -86,16 +80,11 @@ module FactoryBot
86
80
  end
87
81
 
88
82
  def register_default_strategies
89
- DEFAULT_STRATEGIES.each { |name, klass| register_strategy(name, klass) }
90
- end
91
-
92
- def register_default_callbacks
93
- DEFAULT_CALLBACKS.each(&method(:register_callback))
94
- end
95
-
96
- def register_callback(name)
97
- name = name.to_sym
98
- callback_names << name
83
+ register_strategy(:build, FactoryBot::Strategy::Build)
84
+ register_strategy(:create, FactoryBot::Strategy::Create)
85
+ register_strategy(:attributes_for, FactoryBot::Strategy::AttributesFor)
86
+ register_strategy(:build_stubbed, FactoryBot::Strategy::Stub)
87
+ register_strategy(:null, FactoryBot::Strategy::Null)
99
88
  end
100
89
  end
101
90
  end