factory_bot 5.0.2 → 6.2.1

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +58 -13
  3. data/GETTING_STARTED.md +745 -149
  4. data/NEWS.md +53 -0
  5. data/README.md +19 -18
  6. data/lib/factory_bot/aliases.rb +4 -4
  7. data/lib/factory_bot/attribute/association.rb +2 -2
  8. data/lib/factory_bot/attribute/dynamic.rb +3 -2
  9. data/lib/factory_bot/attribute_assigner.rb +10 -10
  10. data/lib/factory_bot/attribute_list.rb +1 -1
  11. data/lib/factory_bot/callback.rb +3 -11
  12. data/lib/factory_bot/configuration.rb +6 -6
  13. data/lib/factory_bot/declaration/association.rb +30 -2
  14. data/lib/factory_bot/declaration/implicit.rb +4 -1
  15. data/lib/factory_bot/declaration.rb +1 -1
  16. data/lib/factory_bot/declaration_list.rb +2 -2
  17. data/lib/factory_bot/decorator/invocation_tracker.rb +2 -1
  18. data/lib/factory_bot/decorator.rb +18 -6
  19. data/lib/factory_bot/definition.rb +66 -19
  20. data/lib/factory_bot/definition_hierarchy.rb +1 -11
  21. data/lib/factory_bot/definition_proxy.rb +77 -12
  22. data/lib/factory_bot/enum.rb +27 -0
  23. data/lib/factory_bot/errors.rb +3 -0
  24. data/lib/factory_bot/evaluator.rb +8 -9
  25. data/lib/factory_bot/evaluator_class_definer.rb +1 -1
  26. data/lib/factory_bot/factory.rb +13 -13
  27. data/lib/factory_bot/factory_runner.rb +4 -4
  28. data/lib/factory_bot/find_definitions.rb +1 -1
  29. data/lib/factory_bot/internal.rb +68 -1
  30. data/lib/factory_bot/linter.rb +9 -13
  31. data/lib/factory_bot/null_factory.rb +10 -4
  32. data/lib/factory_bot/null_object.rb +2 -6
  33. data/lib/factory_bot/registry.rb +4 -4
  34. data/lib/factory_bot/reload.rb +1 -2
  35. data/lib/factory_bot/sequence.rb +5 -5
  36. data/lib/factory_bot/strategy/attributes_for.rb +4 -0
  37. data/lib/factory_bot/strategy/build.rb +4 -0
  38. data/lib/factory_bot/strategy/create.rb +4 -0
  39. data/lib/factory_bot/strategy/null.rb +8 -2
  40. data/lib/factory_bot/strategy/stub.rb +20 -5
  41. data/lib/factory_bot/strategy_calculator.rb +1 -1
  42. data/lib/factory_bot/strategy_syntax_method_registrar.rb +12 -1
  43. data/lib/factory_bot/syntax/default.rb +12 -24
  44. data/lib/factory_bot/syntax/methods.rb +3 -3
  45. data/lib/factory_bot/trait.rb +5 -3
  46. data/lib/factory_bot/version.rb +1 -1
  47. data/lib/factory_bot.rb +21 -93
  48. metadata +12 -39
@@ -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
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
27
- FactoryBot.use_parent_strategy ? @build_strategy.class : :create
28
- end
26
+ strategy_override = overrides.fetch(:strategy) {
27
+ FactoryBot.use_parent_strategy ? @build_strategy.to_sym : :create
28
+ }
29
29
 
30
30
  traits_and_overrides += [overrides.except(:strategy)]
31
31
 
@@ -33,17 +33,16 @@ 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
38
+ def method_missing(method_name, *args, &block)
41
39
  if @instance.respond_to?(method_name)
42
40
  @instance.send(method_name, *args, &block)
43
41
  else
44
42
  SyntaxRunner.new.send(method_name, *args, &block)
45
43
  end
46
44
  end
45
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
47
46
 
48
47
  def respond_to_missing?(method_name, _include_private = false)
49
48
  @instance.respond_to?(method_name) || SyntaxRunner.new.respond_to?(method_name)
@@ -66,7 +65,7 @@ module FactoryBot
66
65
  end
67
66
 
68
67
  def self.define_attribute(name, &block)
69
- if method_defined?(name) || private_method_defined?(name)
68
+ if instance_methods(false).include?(name) || private_instance_methods(false).include?(name)
70
69
  undef_method(name)
71
70
  end
72
71
 
@@ -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
@@ -145,7 +145,7 @@ module FactoryBot
145
145
 
146
146
  def parent
147
147
  if @parent
148
- FactoryBot.factory_by_name(@parent)
148
+ FactoryBot::Internal.factory_by_name(@parent)
149
149
  else
150
150
  NullFactory.new
151
151
  end
@@ -1,15 +1,15 @@
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)
12
- factory = FactoryBot.factory_by_name(@name)
12
+ factory = FactoryBot::Internal.factory_by_name(@name)
13
13
 
14
14
  factory.compile
15
15
 
@@ -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) }
@@ -2,7 +2,19 @@ module FactoryBot
2
2
  # @api private
3
3
  module Internal
4
4
  class << self
5
- delegate :inline_sequences, 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
6
18
 
7
19
  def configuration
8
20
  @configuration ||= Configuration.new
@@ -19,6 +31,61 @@ module FactoryBot
19
31
  def rewind_inline_sequences
20
32
  inline_sequences.each(&:rewind)
21
33
  end
34
+
35
+ def register_trait(trait)
36
+ trait.names.each do |name|
37
+ traits.register(name, trait)
38
+ end
39
+ trait
40
+ end
41
+
42
+ def trait_by_name(name)
43
+ traits.find(name)
44
+ end
45
+
46
+ def register_sequence(sequence)
47
+ sequence.names.each do |name|
48
+ sequences.register(name, sequence)
49
+ end
50
+ sequence
51
+ end
52
+
53
+ def sequence_by_name(name)
54
+ sequences.find(name)
55
+ end
56
+
57
+ def rewind_sequences
58
+ sequences.each(&:rewind)
59
+ rewind_inline_sequences
60
+ end
61
+
62
+ def register_factory(factory)
63
+ factory.names.each do |name|
64
+ factories.register(name, factory)
65
+ end
66
+ factory
67
+ end
68
+
69
+ def factory_by_name(name)
70
+ factories.find(name)
71
+ end
72
+
73
+ def register_strategy(strategy_name, strategy_class)
74
+ strategies.register(strategy_name, strategy_class)
75
+ StrategySyntaxMethodRegistrar.new(strategy_name).define_strategy_methods
76
+ end
77
+
78
+ def strategy_by_name(name)
79
+ strategies.find(name)
80
+ end
81
+
82
+ def register_default_strategies
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)
88
+ end
22
89
  end
23
90
  end
24
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,8 +71,8 @@ module FactoryBot
72
71
  result = []
73
72
  begin
74
73
  FactoryBot.public_send(factory_strategy, factory.name)
75
- rescue StandardError => error
76
- result |= [FactoryError.new(error, factory)]
74
+ rescue => e
75
+ result |= [FactoryError.new(e, factory)]
77
76
  end
78
77
  result
79
78
  end
@@ -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 => error
87
- result |=
88
- [FactoryTraitError.new(error, 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:
@@ -10,12 +10,18 @@ module FactoryBot
10
10
  delegate :defined_traits, :callbacks, :attributes, :constructor,
11
11
  :to_create, to: :definition
12
12
 
13
- def compile; end
13
+ def compile
14
+ end
14
15
 
15
- def class_name; end
16
+ def class_name
17
+ end
16
18
 
17
- def evaluator_class; FactoryBot::Evaluator; end
19
+ def evaluator_class
20
+ FactoryBot::Evaluator
21
+ end
18
22
 
19
- def hierarchy_class; FactoryBot::DefinitionHierarchy; end
23
+ def hierarchy_class
24
+ FactoryBot::DefinitionHierarchy
25
+ end
20
26
  end
21
27
  end
@@ -5,7 +5,7 @@ module FactoryBot
5
5
  @methods_to_respond_to = methods_to_respond_to.map(&:to_s)
6
6
  end
7
7
 
8
- def method_missing(name, *args, &block)
8
+ def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing
9
9
  if respond_to?(name)
10
10
  nil
11
11
  else
@@ -13,12 +13,8 @@ module FactoryBot
13
13
  end
14
14
  end
15
15
 
16
- def respond_to?(method, _include_private = false)
16
+ def respond_to?(method)
17
17
  @methods_to_respond_to.include? method.to_s
18
18
  end
19
-
20
- def respond_to_missing?(*)
21
- false
22
- end
23
19
  end
24
20
  end
@@ -7,7 +7,7 @@ module FactoryBot
7
7
  attr_reader :name
8
8
 
9
9
  def initialize(name)
10
- @name = name
10
+ @name = name
11
11
  @items = ActiveSupport::HashWithIndifferentAccess.new
12
12
  end
13
13
 
@@ -21,11 +21,11 @@ module FactoryBot
21
21
 
22
22
  def find(name)
23
23
  @items.fetch(name)
24
- rescue KeyError => key_error
25
- raise key_error_with_custom_message(key_error)
24
+ rescue KeyError => e
25
+ raise key_error_with_custom_message(e)
26
26
  end
27
27
 
28
- alias :[] :find
28
+ alias_method :[], :find
29
29
 
30
30
  def register(name, item)
31
31
  @items[name] = item
@@ -1,8 +1,7 @@
1
1
  module FactoryBot
2
2
  def self.reload
3
3
  Internal.reset_configuration
4
- register_default_strategies
5
- register_default_callbacks
4
+ Internal.register_default_strategies
6
5
  find_definitions
7
6
  end
8
7
  end
@@ -6,14 +6,14 @@ module FactoryBot
6
6
  attr_reader :name
7
7
 
8
8
  def initialize(name, *args, &proc)
9
- @name = name
10
- @proc = proc
9
+ @name = name
10
+ @proc = proc
11
11
 
12
- options = args.extract_options!
13
- @value = args.first || 1
12
+ options = args.extract_options!
13
+ @value = args.first || 1
14
14
  @aliases = options.fetch(:aliases) { [] }
15
15
 
16
- if !@value.respond_to?(:peek)
16
+ unless @value.respond_to?(:peek)
17
17
  @value = EnumeratorAdapter.new(@value)
18
18
  end
19
19
  end
@@ -8,6 +8,10 @@ module FactoryBot
8
8
  def result(evaluation)
9
9
  evaluation.hash
10
10
  end
11
+
12
+ def to_sym
13
+ :attributes_for
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -10,6 +10,10 @@ module FactoryBot
10
10
  evaluation.notify(:after_build, instance)
11
11
  end
12
12
  end
13
+
14
+ def to_sym
15
+ :build
16
+ end
13
17
  end
14
18
  end
15
19
  end
@@ -13,6 +13,10 @@ module FactoryBot
13
13
  evaluation.notify(:after_create, instance)
14
14
  end
15
15
  end
16
+
17
+ def to_sym
18
+ :create
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -1,9 +1,15 @@
1
1
  module FactoryBot
2
2
  module Strategy
3
3
  class Null
4
- def association(runner); end
4
+ def association(runner)
5
+ end
5
6
 
6
- def result(evaluation); end
7
+ def result(evaluation)
8
+ end
9
+
10
+ def to_sym
11
+ :null
12
+ end
7
13
  end
8
14
  end
9
15
  end