factory_bot 5.0.1 → 6.2.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +58 -13
  3. data/GETTING_STARTED.md +705 -149
  4. data/NEWS.md +51 -1
  5. data/README.md +19 -18
  6. data/lib/factory_bot.rb +21 -93
  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 +3 -2
  10. data/lib/factory_bot/attribute_assigner.rb +8 -9
  11. data/lib/factory_bot/attribute_list.rb +1 -1
  12. data/lib/factory_bot/callback.rb +3 -11
  13. data/lib/factory_bot/configuration.rb +6 -6
  14. data/lib/factory_bot/declaration.rb +1 -1
  15. data/lib/factory_bot/declaration/association.rb +30 -2
  16. data/lib/factory_bot/declaration/implicit.rb +4 -1
  17. data/lib/factory_bot/declaration_list.rb +2 -2
  18. data/lib/factory_bot/decorator.rb +18 -6
  19. data/lib/factory_bot/decorator/invocation_tracker.rb +2 -1
  20. data/lib/factory_bot/definition.rb +66 -19
  21. data/lib/factory_bot/definition_hierarchy.rb +1 -11
  22. data/lib/factory_bot/definition_proxy.rb +77 -12
  23. data/lib/factory_bot/enum.rb +27 -0
  24. data/lib/factory_bot/errors.rb +3 -0
  25. data/lib/factory_bot/evaluator.rb +7 -8
  26. data/lib/factory_bot/evaluator_class_definer.rb +1 -1
  27. data/lib/factory_bot/factory.rb +13 -13
  28. data/lib/factory_bot/factory_runner.rb +4 -4
  29. data/lib/factory_bot/find_definitions.rb +1 -1
  30. data/lib/factory_bot/internal.rb +68 -1
  31. data/lib/factory_bot/linter.rb +9 -13
  32. data/lib/factory_bot/null_factory.rb +10 -4
  33. data/lib/factory_bot/null_object.rb +2 -6
  34. data/lib/factory_bot/registry.rb +4 -4
  35. data/lib/factory_bot/reload.rb +1 -2
  36. data/lib/factory_bot/sequence.rb +5 -5
  37. data/lib/factory_bot/strategy/null.rb +4 -2
  38. data/lib/factory_bot/strategy/stub.rb +16 -5
  39. data/lib/factory_bot/strategy_calculator.rb +1 -1
  40. data/lib/factory_bot/strategy_syntax_method_registrar.rb +12 -1
  41. data/lib/factory_bot/syntax/default.rb +12 -24
  42. data/lib/factory_bot/syntax/methods.rb +3 -3
  43. data/lib/factory_bot/trait.rb +6 -4
  44. data/lib/factory_bot/version.rb +1 -1
  45. metadata +12 -40
@@ -8,7 +8,7 @@ module FactoryBot
8
8
  attr_reader :name
9
9
 
10
10
  def initialize(name, ignored = false)
11
- @name = name
11
+ @name = name
12
12
  @ignored = ignored
13
13
  end
14
14
 
@@ -6,6 +6,7 @@ module FactoryBot
6
6
  super(name, false)
7
7
  @options = options.dup
8
8
  @overrides = options.extract_options!
9
+ @factory_name = @overrides.delete(:factory) || name
9
10
  @traits = options
10
11
  end
11
12
 
@@ -21,9 +22,36 @@ module FactoryBot
21
22
 
22
23
  private
23
24
 
25
+ attr_reader :factory_name, :overrides, :traits
26
+
24
27
  def build
25
- factory_name = @overrides[:factory] || name
26
- [Attribute::Association.new(name, factory_name, [@traits, @overrides.except(:factory)].flatten)]
28
+ raise_if_arguments_are_declarations!
29
+
30
+ [
31
+ Attribute::Association.new(
32
+ name,
33
+ factory_name,
34
+ [traits, overrides].flatten
35
+ )
36
+ ]
37
+ end
38
+
39
+ def raise_if_arguments_are_declarations!
40
+ if factory_name.is_a?(Declaration)
41
+ raise ArgumentError.new(<<~MSG)
42
+ Association '#{name}' received an invalid factory argument.
43
+ Did you mean? 'factory: :#{factory_name.name}'
44
+ MSG
45
+ end
46
+
47
+ overrides.each do |attribute, value|
48
+ if value.is_a?(Declaration)
49
+ raise ArgumentError.new(<<~MSG)
50
+ Association '#{name}' received an invalid attribute override.
51
+ Did you mean? '#{attribute}: :#{value.name}'
52
+ MSG
53
+ end
54
+ end
27
55
  end
28
56
  end
29
57
  end
@@ -23,8 +23,11 @@ module FactoryBot
23
23
  def build
24
24
  if FactoryBot.factories.registered?(name)
25
25
  [Attribute::Association.new(name, name, {})]
26
- elsif FactoryBot.sequences.registered?(name)
26
+ elsif FactoryBot::Internal.sequences.registered?(name)
27
27
  [Attribute::Sequence.new(name, name, @ignored)]
28
+ elsif @factory.name.to_s == name.to_s
29
+ message = "Self-referencing trait '#{@name}'"
30
+ raise TraitDefinitionError, message
28
31
  else
29
32
  @factory.inherit_traits([name])
30
33
  []
@@ -5,8 +5,8 @@ module FactoryBot
5
5
 
6
6
  def initialize(name = nil)
7
7
  @declarations = []
8
- @name = name
9
- @overridable = false
8
+ @name = name
9
+ @overridable = false
10
10
  end
11
11
 
12
12
  def declare_attribute(declaration)
@@ -6,18 +6,30 @@ module FactoryBot
6
6
  @component = component
7
7
  end
8
8
 
9
- def method_missing(name, *args, &block) # rubocop:disable Style/MethodMissing
10
- @component.send(name, *args, &block)
9
+ if ::Gem::Version.new(::RUBY_VERSION) >= ::Gem::Version.new("2.7")
10
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
11
+ def method_missing(...) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
12
+ @component.send(...)
13
+ end
14
+
15
+ def send(...)
16
+ __send__(...)
17
+ end
18
+ RUBY
19
+ else
20
+ def method_missing(name, *args, &block) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
21
+ @component.send(name, *args, &block)
22
+ end
23
+
24
+ def send(symbol, *args, &block)
25
+ __send__(symbol, *args, &block)
26
+ end
11
27
  end
12
28
 
13
29
  def respond_to_missing?(name, include_private = false)
14
30
  @component.respond_to?(name, true) || super
15
31
  end
16
32
 
17
- def send(symbol, *args, &block)
18
- __send__(symbol, *args, &block)
19
- end
20
-
21
33
  def self.const_missing(name)
22
34
  ::Object.const_get(name)
23
35
  end
@@ -6,10 +6,11 @@ module FactoryBot
6
6
  @invoked_methods = []
7
7
  end
8
8
 
9
- def method_missing(name, *args, &block) # rubocop:disable Style/MethodMissing
9
+ def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing
10
10
  @invoked_methods << name
11
11
  super
12
12
  end
13
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
13
14
 
14
15
  def __invoked_methods__
15
16
  @invoked_methods.uniq
@@ -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
@@ -28,7 +30,7 @@ module FactoryBot
28
30
  end
29
31
 
30
32
  def to_create(&block)
31
- if block_given?
33
+ if block
32
34
  @to_create = block
33
35
  else
34
36
  aggregate_from_traits_and_self(:to_create) { @to_create }.last
@@ -43,13 +45,15 @@ 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 }
52
- additional_traits.each { |bt| bt.define_trait defined_trait }
55
+ base_traits.each { |bt| bt.define_trait defined_trait }
56
+ additional_traits.each { |at| at.define_trait defined_trait }
53
57
  end
54
58
 
55
59
  @compiled = true
@@ -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.register_callback(name)
99
106
  add_callback(Callback.new(name, block))
100
107
  end
101
108
  end
@@ -104,6 +111,20 @@ module FactoryBot
104
111
 
105
112
  def base_traits
106
113
  @base_traits.map { |name| trait_by_name(name) }
114
+ rescue KeyError => error
115
+ raise error_with_definition_name(error)
116
+ end
117
+
118
+ def error_with_definition_name(error)
119
+ message = error.message
120
+ message.insert(
121
+ message.index("\nDid you mean?") || message.length,
122
+ " referenced within \"#{name}\" definition"
123
+ )
124
+
125
+ error.class.new(message).tap do |new_error|
126
+ new_error.set_backtrace(error.backtrace)
127
+ end
107
128
  end
108
129
 
109
130
  def additional_traits
@@ -111,17 +132,19 @@ module FactoryBot
111
132
  end
112
133
 
113
134
  def trait_by_name(name)
114
- trait_for(name) || FactoryBot.trait_by_name(name)
135
+ trait_for(name) || Internal.trait_by_name(name)
115
136
  end
116
137
 
117
138
  def trait_for(name)
118
- defined_traits.detect { |trait| trait.name == name }
139
+ @defined_traits_by_name ||= defined_traits.each_with_object({}) { |t, memo| memo[t.name] ||= t }
140
+ @defined_traits_by_name[name.to_s]
119
141
  end
120
142
 
121
143
  def initialize_copy(source)
122
144
  super
123
145
  @attributes = nil
124
- @compiled = false
146
+ @compiled = false
147
+ @defined_traits_by_name = nil
125
148
  end
126
149
 
127
150
  def aggregate_from_traits_and_self(method_name, &block)
@@ -130,8 +153,32 @@ module FactoryBot
130
153
  [
131
154
  base_traits.map(&method_name),
132
155
  instance_exec(&block),
133
- additional_traits.map(&method_name),
156
+ additional_traits.map(&method_name)
134
157
  ].flatten.compact
135
158
  end
159
+
160
+ def expand_enum_traits(klass)
161
+ return if @expanded_enum_traits
162
+
163
+ if automatically_register_defined_enums?(klass)
164
+ automatically_register_defined_enums(klass)
165
+ end
166
+
167
+ registered_enums.each do |enum|
168
+ traits = enum.build_traits(klass)
169
+ traits.each { |trait| define_trait(trait) }
170
+ end
171
+
172
+ @expanded_enum_traits = true
173
+ end
174
+
175
+ def automatically_register_defined_enums(klass)
176
+ klass.defined_enums.each_key { |name| register_enum(Enum.new(name)) }
177
+ end
178
+
179
+ def automatically_register_defined_enums?(klass)
180
+ FactoryBot.automatically_define_enum_traits &&
181
+ klass.respond_to?(:defined_enums)
182
+ end
136
183
  end
137
184
  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,15 +88,18 @@ module FactoryBot
88
88
  # end
89
89
  #
90
90
  # are equivalent.
91
- def method_missing(name, *args, &block) # rubocop:disable Style/MethodMissing
92
- if args.empty?
91
+ def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing, Style/MethodMissingSuper
92
+ association_options = args.first
93
+
94
+ if association_options.nil?
93
95
  __declare_attribute__(name, block)
94
- elsif args.first.respond_to?(:has_key?) && args.first.has_key?(:factory)
95
- association(name, *args)
96
+ elsif __valid_association_options?(association_options)
97
+ association(name, association_options)
96
98
  else
97
- raise NoMethodError.new(
98
- "undefined method '#{name}' in '#{@definition.name}' factory",
99
- )
99
+ raise NoMethodError.new(<<~MSG)
100
+ undefined method '#{name}' in '#{@definition.name}' factory
101
+ Did you mean? '#{name} { #{association_options.inspect} }'
102
+ MSG
100
103
  end
101
104
  end
102
105
 
@@ -149,7 +152,7 @@ module FactoryBot
149
152
  if block_given?
150
153
  raise AssociationDefinitionError.new(
151
154
  "Unexpected block passed to '#{name}' association "\
152
- "in '#{@definition.name}' factory",
155
+ "in '#{@definition.name}' factory"
153
156
  )
154
157
  else
155
158
  declaration = Declaration::Association.new(name, *options)
@@ -173,6 +176,64 @@ module FactoryBot
173
176
  @definition.define_trait(Trait.new(name, &block))
174
177
  end
175
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
+
176
237
  def initialize_with(&block)
177
238
  @definition.define_constructor(&block)
178
239
  end
@@ -187,5 +248,9 @@ module FactoryBot
187
248
  add_attribute(name, &block)
188
249
  end
189
250
  end
251
+
252
+ def __valid_association_options?(options)
253
+ options.respond_to?(:has_key?) && options.has_key?(:factory)
254
+ end
190
255
  end
191
256
  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