factory_bot 5.2.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +52 -13
  3. data/GETTING_STARTED.md +432 -65
  4. data/NEWS.md +23 -2
  5. data/README.md +2 -8
  6. data/lib/factory_bot.rb +19 -53
  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_proxy.rb +63 -5
  21. data/lib/factory_bot/enum.rb +27 -0
  22. data/lib/factory_bot/evaluator.rb +19 -11
  23. data/lib/factory_bot/evaluator_class_definer.rb +1 -1
  24. data/lib/factory_bot/factory.rb +12 -12
  25. data/lib/factory_bot/factory_runner.rb +3 -3
  26. data/lib/factory_bot/find_definitions.rb +1 -1
  27. data/lib/factory_bot/internal.rb +12 -25
  28. data/lib/factory_bot/linter.rb +8 -12
  29. data/lib/factory_bot/null_factory.rb +11 -5
  30. data/lib/factory_bot/null_object.rb +2 -6
  31. data/lib/factory_bot/registry.rb +2 -2
  32. data/lib/factory_bot/reload.rb +0 -1
  33. data/lib/factory_bot/sequence.rb +5 -5
  34. data/lib/factory_bot/strategy/null.rb +4 -2
  35. data/lib/factory_bot/strategy/stub.rb +6 -2
  36. data/lib/factory_bot/syntax/default.rb +6 -6
  37. data/lib/factory_bot/trait.rb +1 -1
  38. data/lib/factory_bot/version.rb +1 -1
  39. metadata +9 -37
@@ -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,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
 
@@ -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) }
@@ -3,19 +3,18 @@ module FactoryBot
3
3
  module Internal
4
4
  class << self
5
5
  delegate :after,
6
- :before,
7
- :callback_names,
8
- :callbacks,
9
- :constructor,
10
- :factories,
11
- :initialize_with,
12
- :inline_sequences,
13
- :sequences,
14
- :skip_create,
15
- :strategies,
16
- :to_create,
17
- :traits,
18
- to: :configuration
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
19
18
 
20
19
  def configuration
21
20
  @configuration ||= Configuration.new
@@ -87,18 +86,6 @@ module FactoryBot
87
86
  register_strategy(:build_stubbed, FactoryBot::Strategy::Stub)
88
87
  register_strategy(:null, FactoryBot::Strategy::Null)
89
88
  end
90
-
91
- def register_default_callbacks
92
- register_callback(:after_create)
93
- register_callback(:after_build)
94
- register_callback(:after_stub)
95
- register_callback(:before_create)
96
- end
97
-
98
- def register_callback(name)
99
- name = name.to_sym
100
- callback_names << name
101
- end
102
89
  end
103
90
  end
104
91
  end
@@ -19,17 +19,16 @@ module FactoryBot
19
19
  attr_reader :factories_to_lint, :invalid_factories, :factory_strategy
20
20
 
21
21
  def calculate_invalid_factories
22
- factories_to_lint.reduce(Hash.new([])) do |result, factory|
22
+ factories_to_lint.each_with_object(Hash.new([])) do |factory, result|
23
23
  errors = lint(factory)
24
24
  result[factory] |= errors unless errors.empty?
25
- result
26
25
  end
27
26
  end
28
27
 
29
28
  class FactoryError
30
29
  def initialize(wrapped_error, factory)
31
30
  @wrapped_error = wrapped_error
32
- @factory = factory
31
+ @factory = factory
33
32
  end
34
33
 
35
34
  def message
@@ -72,7 +71,7 @@ module FactoryBot
72
71
  result = []
73
72
  begin
74
73
  FactoryBot.public_send(factory_strategy, factory.name)
75
- rescue StandardError => e
74
+ rescue => e
76
75
  result |= [FactoryError.new(e, factory)]
77
76
  end
78
77
  result
@@ -81,20 +80,17 @@ module FactoryBot
81
80
  def lint_traits(factory)
82
81
  result = []
83
82
  factory.definition.defined_traits.map(&:name).each do |trait_name|
84
- begin
85
- FactoryBot.public_send(factory_strategy, factory.name, trait_name)
86
- rescue StandardError => e
87
- result |=
88
- [FactoryTraitError.new(e, factory, trait_name)]
89
- end
83
+ FactoryBot.public_send(factory_strategy, factory.name, trait_name)
84
+ rescue => e
85
+ result |= [FactoryTraitError.new(e, factory, trait_name)]
90
86
  end
91
87
  result
92
88
  end
93
89
 
94
90
  def error_message
95
- lines = invalid_factories.map do |_factory, exceptions|
91
+ lines = invalid_factories.map { |_factory, exceptions|
96
92
  exceptions.map(&error_message_type)
97
- end.flatten
93
+ }.flatten
98
94
 
99
95
  <<~ERROR_MESSAGE.strip
100
96
  The following factories are invalid: