factory_bot 4.11.1 → 6.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +58 -13
  3. data/GETTING_STARTED.md +825 -153
  4. data/LICENSE +1 -1
  5. data/NEWS.md +385 -0
  6. data/README.md +20 -30
  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.rb +4 -39
  11. data/lib/factory_bot/attribute_assigner.rb +27 -12
  12. data/lib/factory_bot/attribute_list.rb +3 -2
  13. data/lib/factory_bot/callback.rb +4 -11
  14. data/lib/factory_bot/configuration.rb +15 -19
  15. data/lib/factory_bot/declaration/association.rb +33 -3
  16. data/lib/factory_bot/declaration/dynamic.rb +3 -1
  17. data/lib/factory_bot/declaration/implicit.rb +7 -2
  18. data/lib/factory_bot/declaration.rb +5 -5
  19. data/lib/factory_bot/declaration_list.rb +3 -3
  20. data/lib/factory_bot/decorator/attribute_hash.rb +1 -1
  21. data/lib/factory_bot/decorator/invocation_tracker.rb +2 -1
  22. data/lib/factory_bot/decorator.rb +20 -4
  23. data/lib/factory_bot/definition.rb +69 -21
  24. data/lib/factory_bot/definition_hierarchy.rb +1 -11
  25. data/lib/factory_bot/definition_proxy.rb +119 -64
  26. data/lib/factory_bot/enum.rb +27 -0
  27. data/lib/factory_bot/errors.rb +7 -4
  28. data/lib/factory_bot/evaluation.rb +1 -1
  29. data/lib/factory_bot/evaluator.rb +10 -11
  30. data/lib/factory_bot/evaluator_class_definer.rb +1 -1
  31. data/lib/factory_bot/factory.rb +12 -12
  32. data/lib/factory_bot/factory_runner.rb +4 -4
  33. data/lib/factory_bot/find_definitions.rb +2 -2
  34. data/lib/factory_bot/internal.rb +91 -0
  35. data/lib/factory_bot/linter.rb +41 -28
  36. data/lib/factory_bot/null_factory.rb +13 -4
  37. data/lib/factory_bot/null_object.rb +2 -6
  38. data/lib/factory_bot/registry.rb +17 -8
  39. data/lib/factory_bot/reload.rb +2 -3
  40. data/lib/factory_bot/sequence.rb +5 -6
  41. data/lib/factory_bot/strategy/attributes_for.rb +4 -0
  42. data/lib/factory_bot/strategy/build.rb +4 -0
  43. data/lib/factory_bot/strategy/create.rb +4 -0
  44. data/lib/factory_bot/strategy/null.rb +4 -0
  45. data/lib/factory_bot/strategy/stub.rb +41 -32
  46. data/lib/factory_bot/strategy_calculator.rb +1 -1
  47. data/lib/factory_bot/strategy_syntax_method_registrar.rb +13 -2
  48. data/lib/factory_bot/syntax/default.rb +13 -25
  49. data/lib/factory_bot/syntax/methods.rb +32 -9
  50. data/lib/factory_bot/syntax.rb +2 -2
  51. data/lib/factory_bot/trait.rb +7 -4
  52. data/lib/factory_bot/version.rb +1 -1
  53. data/lib/factory_bot.rb +71 -140
  54. metadata +46 -34
  55. data/NEWS +0 -306
  56. data/lib/factory_bot/attribute/static.rb +0 -16
  57. data/lib/factory_bot/declaration/static.rb +0 -26
  58. data/lib/factory_bot/decorator/class_key_hash.rb +0 -28
@@ -4,7 +4,7 @@ module FactoryBot
4
4
  include Enumerable
5
5
 
6
6
  def initialize(name = nil, attributes = [])
7
- @name = name
7
+ @name = name
8
8
  @attributes = attributes
9
9
  end
10
10
 
@@ -54,7 +54,8 @@ module FactoryBot
54
54
 
55
55
  def ensure_attribute_not_self_referencing!(attribute)
56
56
  if attribute.respond_to?(:factory) && attribute.factory == @name
57
- raise AssociationDefinitionError, "Self-referencing association '#{attribute.name}' in '#{attribute.factory}'"
57
+ message = "Self-referencing association '#{attribute.name}' in '#{attribute.factory}'"
58
+ raise AssociationDefinitionError, message
58
59
  end
59
60
  end
60
61
 
@@ -3,16 +3,15 @@ module FactoryBot
3
3
  attr_reader :name
4
4
 
5
5
  def initialize(name, block)
6
- @name = name.to_sym
6
+ @name = name.to_sym
7
7
  @block = block
8
- ensure_valid_callback_name!
9
8
  end
10
9
 
11
10
  def run(instance, evaluator)
12
11
  case block.arity
13
- when 1, -1 then syntax_runner.instance_exec(instance, &block)
12
+ when 1, -1, -2 then syntax_runner.instance_exec(instance, &block)
14
13
  when 2 then syntax_runner.instance_exec(instance, evaluator, &block)
15
- else syntax_runner.instance_exec(&block)
14
+ else syntax_runner.instance_exec(&block)
16
15
  end
17
16
  end
18
17
 
@@ -22,17 +21,11 @@ module FactoryBot
22
21
  end
23
22
 
24
23
  protected
24
+
25
25
  attr_reader :block
26
26
 
27
27
  private
28
28
 
29
- def ensure_valid_callback_name!
30
- unless FactoryBot.callback_names.include?(name)
31
- raise InvalidCallbackNameError, "#{name} is not a valid callback name. " +
32
- "Valid callback names are #{FactoryBot.callback_names.inspect}"
33
- end
34
- end
35
-
36
29
  def syntax_runner
37
30
  @syntax_runner ||= SyntaxRunner.new
38
31
  end
@@ -1,21 +1,25 @@
1
1
  module FactoryBot
2
2
  # @api private
3
3
  class Configuration
4
- attr_reader :factories, :sequences, :traits, :strategies, :callback_names
5
-
6
- attr_accessor :allow_class_lookup, :use_parent_strategy
4
+ attr_reader(
5
+ :callback_names,
6
+ :factories,
7
+ :inline_sequences,
8
+ :sequences,
9
+ :strategies,
10
+ :traits
11
+ )
7
12
 
8
13
  def initialize
9
- @factories = Decorator::DisallowsDuplicatesRegistry.new(Registry.new('Factory'))
10
- @sequences = Decorator::DisallowsDuplicatesRegistry.new(Registry.new('Sequence'))
11
- @traits = Decorator::DisallowsDuplicatesRegistry.new(Registry.new('Trait'))
12
- @strategies = Registry.new('Strategy')
14
+ @factories = Decorator::DisallowsDuplicatesRegistry.new(Registry.new("Factory"))
15
+ @sequences = Decorator::DisallowsDuplicatesRegistry.new(Registry.new("Sequence"))
16
+ @traits = Decorator::DisallowsDuplicatesRegistry.new(Registry.new("Trait"))
17
+ @strategies = Registry.new("Strategy")
13
18
  @callback_names = Set.new
14
- @definition = Definition.new
15
-
16
- @allow_class_lookup = true
19
+ @definition = Definition.new(:configuration)
20
+ @inline_sequences = []
17
21
 
18
- to_create { |instance| instance.save! }
22
+ to_create(&:save!)
19
23
  initialize_with { new }
20
24
  end
21
25
 
@@ -25,13 +29,5 @@ module FactoryBot
25
29
  def initialize_with(&block)
26
30
  @definition.define_constructor(&block)
27
31
  end
28
-
29
- def duplicate_attribute_assignment_from_initialize_with
30
- false
31
- end
32
-
33
- def duplicate_attribute_assignment_from_initialize_with=(value)
34
- ActiveSupport::Deprecation.warn 'Assignment of duplicate_attribute_assignment_from_initialize_with is unnecessary as this is now default behavior in FactoryBot 4.0; this line can be removed', caller
35
- end
36
32
  end
37
33
  end
@@ -6,22 +6,52 @@ 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
 
12
13
  def ==(other)
13
- name == other.name &&
14
+ self.class == other.class &&
15
+ name == other.name &&
14
16
  options == other.options
15
17
  end
16
18
 
17
19
  protected
20
+
18
21
  attr_reader :options
19
22
 
20
23
  private
21
24
 
25
+ attr_reader :factory_name, :overrides, :traits
26
+
22
27
  def build
23
- factory_name = @overrides[:factory] || name
24
- [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
25
55
  end
26
56
  end
27
57
  end
@@ -8,12 +8,14 @@ module FactoryBot
8
8
  end
9
9
 
10
10
  def ==(other)
11
- name == other.name &&
11
+ self.class == other.class &&
12
+ name == other.name &&
12
13
  ignored == other.ignored &&
13
14
  block == other.block
14
15
  end
15
16
 
16
17
  protected
18
+
17
19
  attr_reader :block
18
20
 
19
21
  private
@@ -8,12 +8,14 @@ module FactoryBot
8
8
  end
9
9
 
10
10
  def ==(other)
11
- name == other.name &&
11
+ self.class == other.class &&
12
+ name == other.name &&
12
13
  factory == other.factory &&
13
14
  ignored == other.ignored
14
15
  end
15
16
 
16
17
  protected
18
+
17
19
  attr_reader :factory
18
20
 
19
21
  private
@@ -21,8 +23,11 @@ module FactoryBot
21
23
  def build
22
24
  if FactoryBot.factories.registered?(name)
23
25
  [Attribute::Association.new(name, name, {})]
24
- elsif FactoryBot.sequences.registered?(name)
26
+ elsif FactoryBot::Internal.sequences.registered?(name)
25
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
26
31
  else
27
32
  @factory.inherit_traits([name])
28
33
  []
@@ -1,7 +1,6 @@
1
- require 'factory_bot/declaration/static'
2
- require 'factory_bot/declaration/dynamic'
3
- require 'factory_bot/declaration/association'
4
- require 'factory_bot/declaration/implicit'
1
+ require "factory_bot/declaration/dynamic"
2
+ require "factory_bot/declaration/association"
3
+ require "factory_bot/declaration/implicit"
5
4
 
6
5
  module FactoryBot
7
6
  # @api private
@@ -9,7 +8,7 @@ module FactoryBot
9
8
  attr_reader :name
10
9
 
11
10
  def initialize(name, ignored = false)
12
- @name = name
11
+ @name = name
13
12
  @ignored = ignored
14
13
  end
15
14
 
@@ -18,6 +17,7 @@ module FactoryBot
18
17
  end
19
18
 
20
19
  protected
20
+
21
21
  attr_reader :ignored
22
22
  end
23
23
  end
@@ -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)
@@ -39,7 +39,7 @@ module FactoryBot
39
39
  end
40
40
 
41
41
  def to_attributes
42
- @declarations.inject([]) { |result, declaration| result += declaration.to_attributes }
42
+ @declarations.reduce([]) { |result, declaration| result + declaration.to_attributes }
43
43
  end
44
44
 
45
45
  def overridable?
@@ -8,7 +8,7 @@ module FactoryBot
8
8
 
9
9
  def attributes
10
10
  @attributes.each_with_object({}) do |attribute_name, result|
11
- result[attribute_name] = send(attribute_name)
11
+ result[attribute_name] = @component.send(attribute_name)
12
12
  end
13
13
  end
14
14
  end
@@ -6,10 +6,11 @@ module FactoryBot
6
6
  @invoked_methods = []
7
7
  end
8
8
 
9
- def method_missing(name, *args, &block)
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
@@ -6,12 +6,28 @@ module FactoryBot
6
6
  @component = component
7
7
  end
8
8
 
9
- def method_missing(name, *args, &block)
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/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
- def send(symbol, *args, &block)
14
- __send__(symbol, *args, &block)
29
+ def respond_to_missing?(name, include_private = false)
30
+ @component.respond_to?(name, true) || super
15
31
  end
16
32
 
17
33
  def self.const_missing(name)
@@ -1,18 +1,21 @@
1
1
  module FactoryBot
2
2
  # @api private
3
3
  class Definition
4
- attr_reader :defined_traits, :declarations
5
-
6
- def initialize(name = nil, base_traits = [])
7
- @declarations = DeclarationList.new(name)
8
- @callbacks = []
9
- @defined_traits = Set.new
10
- @to_create = nil
11
- @base_traits = base_traits
4
+ attr_reader :defined_traits, :declarations, :name, :registered_enums
5
+
6
+ def initialize(name, 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
12
14
  @additional_traits = []
13
- @constructor = nil
14
- @attributes = nil
15
- @compiled = false
15
+ @constructor = nil
16
+ @attributes = nil
17
+ @compiled = false
18
+ @expanded_enum_traits = false
16
19
  end
17
20
 
18
21
  delegate :declare_attribute, to: :declarations
@@ -27,7 +30,7 @@ module FactoryBot
27
30
  end
28
31
 
29
32
  def to_create(&block)
30
- if block_given?
33
+ if block
31
34
  @to_create = block
32
35
  else
33
36
  aggregate_from_traits_and_self(:to_create) { @to_create }.last
@@ -42,13 +45,15 @@ module FactoryBot
42
45
  aggregate_from_traits_and_self(:callbacks) { @callbacks }
43
46
  end
44
47
 
45
- def compile
48
+ def compile(klass = nil)
46
49
  unless @compiled
50
+ expand_enum_traits(klass) unless klass.nil?
51
+
47
52
  declarations.attributes
48
53
 
49
54
  defined_traits.each do |defined_trait|
50
- base_traits.each { |bt| bt.define_trait defined_trait }
51
- 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 }
52
57
  end
53
58
 
54
59
  @compiled = true
@@ -73,13 +78,17 @@ module FactoryBot
73
78
  end
74
79
 
75
80
  def skip_create
76
- @to_create = ->(instance) { }
81
+ @to_create = ->(instance) {}
77
82
  end
78
83
 
79
84
  def define_trait(trait)
80
85
  @defined_traits.add(trait)
81
86
  end
82
87
 
88
+ def register_enum(enum)
89
+ @registered_enums << enum
90
+ end
91
+
83
92
  def define_constructor(&block)
84
93
  @constructor = block
85
94
  end
@@ -94,7 +103,6 @@ module FactoryBot
94
103
 
95
104
  def callback(*names, &block)
96
105
  names.each do |name|
97
- FactoryBot.register_callback(name)
98
106
  add_callback(Callback.new(name, block))
99
107
  end
100
108
  end
@@ -103,6 +111,20 @@ module FactoryBot
103
111
 
104
112
  def base_traits
105
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
106
128
  end
107
129
 
108
130
  def additional_traits
@@ -110,17 +132,19 @@ module FactoryBot
110
132
  end
111
133
 
112
134
  def trait_by_name(name)
113
- trait_for(name) || FactoryBot.trait_by_name(name)
135
+ trait_for(name) || Internal.trait_by_name(name)
114
136
  end
115
137
 
116
138
  def trait_for(name)
117
- 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]
118
141
  end
119
142
 
120
143
  def initialize_copy(source)
121
144
  super
122
145
  @attributes = nil
123
- @compiled = false
146
+ @compiled = false
147
+ @defined_traits_by_name = nil
124
148
  end
125
149
 
126
150
  def aggregate_from_traits_and_self(method_name, &block)
@@ -129,8 +153,32 @@ module FactoryBot
129
153
  [
130
154
  base_traits.map(&method_name),
131
155
  instance_exec(&block),
132
- additional_traits.map(&method_name),
156
+ additional_traits.map(&method_name)
133
157
  ].flatten.compact
134
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
135
183
  end
136
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)