factory_bot 5.0.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +52 -13
  3. data/GETTING_STARTED.md +453 -78
  4. data/NEWS.md +40 -0
  5. data/README.md +17 -16
  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 +2 -1
  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 +2 -10
  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 +10 -3
  20. data/lib/factory_bot/definition.rb +51 -18
  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 +20 -12
  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 +11 -23
  42. data/lib/factory_bot/syntax/methods.rb +3 -3
  43. data/lib/factory_bot/trait.rb +5 -3
  44. data/lib/factory_bot/version.rb +1 -1
  45. metadata +9 -36
@@ -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,9 +6,16 @@ module FactoryBot
6
6
  @invoked_methods = []
7
7
  end
8
8
 
9
- def method_missing(name, *args, &block) # rubocop:disable Style/MethodMissing
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,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
@@ -111,17 +118,19 @@ module FactoryBot
111
118
  end
112
119
 
113
120
  def trait_by_name(name)
114
- trait_for(name) || FactoryBot.trait_by_name(name)
121
+ trait_for(name) || Internal.trait_by_name(name)
115
122
  end
116
123
 
117
124
  def trait_for(name)
118
- defined_traits.detect { |trait| trait.name == name.to_s }
125
+ @defined_traits_by_name ||= defined_traits.each_with_object({}) { |t, memo| memo[t.name] ||= t }
126
+ @defined_traits_by_name[name.to_s]
119
127
  end
120
128
 
121
129
  def initialize_copy(source)
122
130
  super
123
131
  @attributes = nil
124
- @compiled = false
132
+ @compiled = false
133
+ @defined_traits_by_name = nil
125
134
  end
126
135
 
127
136
  def aggregate_from_traits_and_self(method_name, &block)
@@ -130,8 +139,32 @@ module FactoryBot
130
139
  [
131
140
  base_traits.map(&method_name),
132
141
  instance_exec(&block),
133
- additional_traits.map(&method_name),
142
+ additional_traits.map(&method_name)
134
143
  ].flatten.compact
135
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
136
169
  end
137
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,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
@@ -2,6 +2,9 @@ module FactoryBot
2
2
  # Raised when a factory is defined that attempts to instantiate itself.
3
3
  class AssociationDefinitionError < RuntimeError; end
4
4
 
5
+ # Raised when a trait is defined that references itself.
6
+ class TraitDefinitionError < RuntimeError; end
7
+
5
8
  # Raised when a callback is defined that has an invalid name
6
9
  class InvalidCallbackNameError < RuntimeError; end
7
10
 
@@ -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/MethodMissing
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
 
@@ -66,7 +74,7 @@ module FactoryBot
66
74
  end
67
75
 
68
76
  def self.define_attribute(name, &block)
69
- if method_defined?(name) || private_method_defined?(name)
77
+ if instance_methods(false).include?(name) || private_instance_methods(false).include?(name)
70
78
  undef_method(name)
71
79
  end
72
80