factory_bot 4.11.1 → 5.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/GETTING_STARTED.md +105 -29
  3. data/NEWS +17 -0
  4. data/README.md +3 -14
  5. data/lib/factory_bot.rb +56 -56
  6. data/lib/factory_bot/aliases.rb +1 -1
  7. data/lib/factory_bot/attribute.rb +4 -39
  8. data/lib/factory_bot/attribute_assigner.rb +20 -5
  9. data/lib/factory_bot/attribute_list.rb +2 -1
  10. data/lib/factory_bot/callback.rb +1 -0
  11. data/lib/factory_bot/configuration.rb +6 -18
  12. data/lib/factory_bot/declaration.rb +4 -4
  13. data/lib/factory_bot/declaration/association.rb +3 -1
  14. data/lib/factory_bot/declaration/dynamic.rb +3 -1
  15. data/lib/factory_bot/declaration/implicit.rb +3 -1
  16. data/lib/factory_bot/declaration_list.rb +1 -1
  17. data/lib/factory_bot/decorator.rb +5 -1
  18. data/lib/factory_bot/decorator/attribute_hash.rb +1 -1
  19. data/lib/factory_bot/decorator/invocation_tracker.rb +1 -1
  20. data/lib/factory_bot/definition.rb +4 -3
  21. data/lib/factory_bot/definition_proxy.rb +53 -62
  22. data/lib/factory_bot/errors.rb +4 -4
  23. data/lib/factory_bot/evaluation.rb +1 -1
  24. data/lib/factory_bot/evaluator.rb +4 -4
  25. data/lib/factory_bot/factory.rb +7 -7
  26. data/lib/factory_bot/factory_runner.rb +3 -3
  27. data/lib/factory_bot/find_definitions.rb +1 -1
  28. data/lib/factory_bot/linter.rb +35 -18
  29. data/lib/factory_bot/null_factory.rb +3 -0
  30. data/lib/factory_bot/null_object.rb +2 -2
  31. data/lib/factory_bot/registry.rb +15 -6
  32. data/lib/factory_bot/sequence.rb +0 -1
  33. data/lib/factory_bot/strategy/null.rb +2 -4
  34. data/lib/factory_bot/strategy/stub.rb +23 -29
  35. data/lib/factory_bot/strategy_syntax_method_registrar.rb +2 -2
  36. data/lib/factory_bot/syntax.rb +2 -2
  37. data/lib/factory_bot/syntax/default.rb +1 -1
  38. data/lib/factory_bot/trait.rb +2 -1
  39. data/lib/factory_bot/version.rb +1 -1
  40. metadata +68 -29
  41. data/lib/factory_bot/attribute/static.rb +0 -16
  42. data/lib/factory_bot/declaration/static.rb +0 -26
  43. data/lib/factory_bot/decorator/class_key_hash.rb +0 -28
@@ -5,7 +5,7 @@ module FactoryBot
5
5
 
6
6
  self.aliases = [
7
7
  [/(.+)_id/, '\1'],
8
- [/(.*)/, '\1_id']
8
+ [/(.*)/, '\1_id'],
9
9
  ]
10
10
 
11
11
  def self.aliases_for(attribute)
@@ -1,7 +1,6 @@
1
- require 'factory_bot/attribute/static'
2
- require 'factory_bot/attribute/dynamic'
3
- require 'factory_bot/attribute/association'
4
- require 'factory_bot/attribute/sequence'
1
+ require "factory_bot/attribute/dynamic"
2
+ require "factory_bot/attribute/association"
3
+ require "factory_bot/attribute/sequence"
5
4
 
6
5
  module FactoryBot
7
6
  # @api private
@@ -11,11 +10,10 @@ module FactoryBot
11
10
  def initialize(name, ignored)
12
11
  @name = name.to_sym
13
12
  @ignored = ignored
14
- ensure_non_attribute_writer!
15
13
  end
16
14
 
17
15
  def to_proc
18
- -> { }
16
+ -> {}
19
17
  end
20
18
 
21
19
  def association?
@@ -25,38 +23,5 @@ module FactoryBot
25
23
  def alias_for?(attr)
26
24
  FactoryBot.aliases_for(attr).include?(name)
27
25
  end
28
-
29
- private
30
-
31
- def ensure_non_attribute_writer!
32
- NonAttributeWriterValidator.new(@name).validate!
33
- end
34
-
35
- class NonAttributeWriterValidator
36
- def initialize(method_name)
37
- @method_name = method_name.to_s
38
- @method_name_setter_match = @method_name.match(/(.*)=$/)
39
- end
40
-
41
- def validate!
42
- if method_is_writer?
43
- raise AttributeDefinitionError, error_message
44
- end
45
- end
46
-
47
- private
48
-
49
- def method_is_writer?
50
- !!@method_name_setter_match
51
- end
52
-
53
- def attribute_name
54
- @method_name_setter_match[1]
55
- end
56
-
57
- def error_message
58
- "factory_bot uses '#{attribute_name} value' syntax rather than '#{attribute_name} = value'"
59
- end
60
- end
61
26
  end
62
27
  end
@@ -22,7 +22,7 @@ module FactoryBot
22
22
  def hash
23
23
  @evaluator.instance = build_hash
24
24
 
25
- attributes_to_set_on_hash.inject({}) do |result, attribute|
25
+ attributes_to_set_on_hash.reduce({}) do |result, attribute|
26
26
  result[attribute] = get(attribute)
27
27
  result
28
28
  end
@@ -31,12 +31,15 @@ module FactoryBot
31
31
  private
32
32
 
33
33
  def method_tracking_evaluator
34
- @method_tracking_evaluator ||= Decorator::AttributeHash.new(decorated_evaluator, attribute_names_to_assign)
34
+ @method_tracking_evaluator ||= Decorator::AttributeHash.new(
35
+ decorated_evaluator,
36
+ attribute_names_to_assign,
37
+ )
35
38
  end
36
39
 
37
40
  def decorated_evaluator
38
41
  Decorator::InvocationTracker.new(
39
- Decorator::NewConstructor.new(@evaluator, @build_class)
42
+ Decorator::NewConstructor.new(@evaluator, @build_class),
40
43
  )
41
44
  end
42
45
 
@@ -65,7 +68,11 @@ module FactoryBot
65
68
  end
66
69
 
67
70
  def attribute_names_to_assign
68
- @attribute_names_to_assign ||= non_ignored_attribute_names + override_names - ignored_attribute_names - alias_names_to_ignore
71
+ @attribute_names_to_assign ||=
72
+ non_ignored_attribute_names +
73
+ override_names -
74
+ ignored_attribute_names -
75
+ alias_names_to_ignore
69
76
  end
70
77
 
71
78
  def non_ignored_attribute_names
@@ -90,8 +97,16 @@ module FactoryBot
90
97
 
91
98
  def alias_names_to_ignore
92
99
  @attribute_list.non_ignored.flat_map do |attribute|
93
- override_names.map { |override| attribute.name if attribute.alias_for?(override) && attribute.name != override && !ignored_attribute_names.include?(override) }
100
+ override_names.map do |override|
101
+ attribute.name if ignorable_alias?(attribute, override)
102
+ end
94
103
  end.compact
95
104
  end
105
+
106
+ def ignorable_alias?(attribute, override)
107
+ attribute.alias_for?(override) &&
108
+ attribute.name != override &&
109
+ !ignored_attribute_names.include?(override)
110
+ end
96
111
  end
97
112
  end
@@ -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
 
@@ -22,6 +22,7 @@ module FactoryBot
22
22
  end
23
23
 
24
24
  protected
25
+
25
26
  attr_reader :block
26
27
 
27
28
  private
@@ -3,19 +3,15 @@ module FactoryBot
3
3
  class Configuration
4
4
  attr_reader :factories, :sequences, :traits, :strategies, :callback_names
5
5
 
6
- attr_accessor :allow_class_lookup, :use_parent_strategy
7
-
8
6
  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')
7
+ @factories = Decorator::DisallowsDuplicatesRegistry.new(Registry.new("Factory"))
8
+ @sequences = Decorator::DisallowsDuplicatesRegistry.new(Registry.new("Sequence"))
9
+ @traits = Decorator::DisallowsDuplicatesRegistry.new(Registry.new("Trait"))
10
+ @strategies = Registry.new("Strategy")
13
11
  @callback_names = Set.new
14
- @definition = Definition.new
15
-
16
- @allow_class_lookup = true
12
+ @definition = Definition.new(:configuration)
17
13
 
18
- to_create { |instance| instance.save! }
14
+ to_create(&:save!)
19
15
  initialize_with { new }
20
16
  end
21
17
 
@@ -25,13 +21,5 @@ module FactoryBot
25
21
  def initialize_with(&block)
26
22
  @definition.define_constructor(&block)
27
23
  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
24
  end
37
25
  end
@@ -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
@@ -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
@@ -10,11 +10,13 @@ module FactoryBot
10
10
  end
11
11
 
12
12
  def ==(other)
13
- name == other.name &&
13
+ self.class == other.class &&
14
+ name == other.name &&
14
15
  options == other.options
15
16
  end
16
17
 
17
18
  protected
19
+
18
20
  attr_reader :options
19
21
 
20
22
  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
  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
@@ -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?
@@ -6,10 +6,14 @@ module FactoryBot
6
6
  @component = component
7
7
  end
8
8
 
9
- def method_missing(name, *args, &block)
9
+ def method_missing(name, *args, &block) # rubocop:disable Style/MethodMissing
10
10
  @component.send(name, *args, &block)
11
11
  end
12
12
 
13
+ def respond_to_missing?(name, include_private = false)
14
+ @component.respond_to?(name, true) || super
15
+ end
16
+
13
17
  def send(symbol, *args, &block)
14
18
  __send__(symbol, *args, &block)
15
19
  end
@@ -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,7 +6,7 @@ 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/MethodMissing
10
10
  @invoked_methods << name
11
11
  super
12
12
  end
@@ -1,9 +1,10 @@
1
1
  module FactoryBot
2
2
  # @api private
3
3
  class Definition
4
- attr_reader :defined_traits, :declarations
4
+ attr_reader :defined_traits, :declarations, :name
5
5
 
6
- def initialize(name = nil, base_traits = [])
6
+ def initialize(name, base_traits = [])
7
+ @name = name
7
8
  @declarations = DeclarationList.new(name)
8
9
  @callbacks = []
9
10
  @defined_traits = Set.new
@@ -73,7 +74,7 @@ module FactoryBot
73
74
  end
74
75
 
75
76
  def skip_create
76
- @to_create = ->(instance) { }
77
+ @to_create = ->(instance) {}
77
78
  end
78
79
 
79
80
  def define_trait(trait)
@@ -1,6 +1,19 @@
1
1
  module FactoryBot
2
2
  class DefinitionProxy
3
- UNPROXIED_METHODS = %w(__send__ __id__ nil? send object_id extend instance_eval initialize block_given? raise caller method)
3
+ UNPROXIED_METHODS = %w(
4
+ __send__
5
+ __id__
6
+ nil?
7
+ send
8
+ object_id
9
+ extend
10
+ instance_eval
11
+ initialize
12
+ block_given?
13
+ raise
14
+ caller
15
+ method
16
+ ).freeze
4
17
 
5
18
  (instance_methods + private_instance_methods).each do |method|
6
19
  undef_method(method) unless UNPROXIED_METHODS.include?(method.to_s)
@@ -21,43 +34,21 @@ module FactoryBot
21
34
  raise FactoryBot::MethodDefinitionError, message
22
35
  end
23
36
 
24
- # Adds an attribute that should be assigned on generated instances for this
25
- # factory.
26
- #
27
- # This method should be called with either a value or block, but not both. If
28
- # called with a block, the attribute will be generated "lazily," whenever an
29
- # instance is generated. Lazy attribute blocks will not be called if that
37
+ # Adds an attribute to the factory.
38
+ # The attribute value will be generated "lazily"
39
+ # by calling the block whenever an instance is generated.
40
+ # The block will not be called if the
30
41
  # attribute is overridden for a specific instance.
31
42
  #
32
- # When defining lazy attributes, an instance of FactoryBot::Strategy will
33
- # be yielded, allowing associations to be built using the correct build
34
- # strategy.
35
- #
36
43
  # Arguments:
37
44
  # * name: +Symbol+ or +String+
38
45
  # The name of this attribute. This will be assigned using "name=" for
39
46
  # generated instances.
40
- # * value: +Object+
41
- # If no block is given, this value will be used for this attribute.
42
- def add_attribute(name, value = nil, &block)
43
- raise AttributeDefinitionError, 'Both value and block given' if value && block_given?
44
-
45
- declaration = if block_given?
46
- Declaration::Dynamic.new(name, @ignore, block)
47
- else
48
- warn_static_attribute_deprecation(name, value)
49
- Declaration::Static.new(name, value, @ignore)
50
- end
51
-
47
+ def add_attribute(name, &block)
48
+ declaration = Declaration::Dynamic.new(name, @ignore, block)
52
49
  @definition.declare_attribute(declaration)
53
50
  end
54
51
 
55
- def ignore(&block)
56
- ActiveSupport::Deprecation.warn "`#ignore` is deprecated and will be "\
57
- "removed in 5.0. Please use `#transient` instead."
58
- transient(&block)
59
- end
60
-
61
52
  def transient(&block)
62
53
  proxy = DefinitionProxy.new(@definition, true)
63
54
  proxy.instance_eval(&block)
@@ -67,40 +58,45 @@ module FactoryBot
67
58
  # attribute, so that:
68
59
  #
69
60
  # factory :user do
70
- # name 'Billy Idol'
61
+ # name { 'Billy Idol' }
71
62
  # end
72
63
  #
73
64
  # and:
74
65
  #
75
66
  # factory :user do
76
- # add_attribute :name, 'Billy Idol'
67
+ # add_attribute(:name) { 'Billy Idol' }
77
68
  # end
78
69
  #
79
70
  # are equivalent.
80
71
  #
81
- # If no argument or block is given, factory_bot will look for a sequence
82
- # or association with the same name. This means that:
72
+ # If no argument or block is given, factory_bot will first look for an
73
+ # association, then for a sequence, and finally for a trait with the same
74
+ # name. This means that given an "admin" trait, an "email" sequence, and an
75
+ # "account" factory:
83
76
  #
84
- # factory :user do
85
- # email { create(:email) }
77
+ # factory :user, traits: [:admin] do
78
+ # email { generate(:email) }
86
79
  # association :account
87
80
  # end
88
81
  #
89
82
  # and:
90
83
  #
91
84
  # factory :user do
85
+ # admin
92
86
  # email
93
87
  # account
94
88
  # end
95
89
  #
96
90
  # are equivalent.
97
- def method_missing(name, *args, &block)
98
- if args.empty? && block.nil?
99
- @definition.declare_attribute(Declaration::Implicit.new(name, @definition, @ignore))
91
+ def method_missing(name, *args, &block) # rubocop:disable Style/MethodMissing
92
+ if args.empty?
93
+ __declare_attribute__(name, block)
100
94
  elsif args.first.respond_to?(:has_key?) && args.first.has_key?(:factory)
101
95
  association(name, *args)
102
96
  else
103
- add_attribute(name, *args, &block)
97
+ raise NoMethodError.new(
98
+ "undefined method '#{name}' in '#{@definition.name}' factory",
99
+ )
104
100
  end
105
101
  end
106
102
 
@@ -121,7 +117,9 @@ module FactoryBot
121
117
  #
122
118
  # Except that no globally available sequence will be defined.
123
119
  def sequence(name, *args, &block)
124
- sequence = Sequence.new(name, *args, &block)
120
+ sequence_name = "__#{@definition.name}_#{name}__"
121
+ sequence = Sequence.new(sequence_name, *args, &block)
122
+ FactoryBot.register_sequence(sequence)
125
123
  add_attribute(name) { increment_sequence(sequence) }
126
124
  end
127
125
 
@@ -149,7 +147,15 @@ module FactoryBot
149
147
  # name of the factory. For example, a "user" association will by
150
148
  # default use the "user" factory.
151
149
  def association(name, *options)
152
- @definition.declare_attribute(Declaration::Association.new(name, *options))
150
+ if block_given?
151
+ raise AssociationDefinitionError.new(
152
+ "Unexpected block passed to '#{name}' association "\
153
+ "in '#{@definition.name}' factory",
154
+ )
155
+ else
156
+ declaration = Declaration::Association.new(name, *options)
157
+ @definition.declare_attribute(declaration)
158
+ end
153
159
  end
154
160
 
155
161
  def to_create(&block)
@@ -174,28 +180,13 @@ module FactoryBot
174
180
 
175
181
  private
176
182
 
177
- def warn_static_attribute_deprecation(name, value)
178
- attribute_caller = caller(2)
179
-
180
- if attribute_caller[0].include?("method_missing")
181
- attribute_caller = caller(3)
183
+ def __declare_attribute__(name, block)
184
+ if block.nil?
185
+ declaration = Declaration::Implicit.new(name, @definition, @ignore)
186
+ @definition.declare_attribute(declaration)
187
+ else
188
+ add_attribute(name, &block)
182
189
  end
183
-
184
- ActiveSupport::Deprecation.warn(<<-MSG, attribute_caller)
185
- Static attributes will be removed in FactoryBot 5.0. Please use dynamic
186
- attributes instead by wrapping the attribute value in a block:
187
-
188
- #{name} { #{value.inspect} }
189
-
190
- To automatically update from static attributes to dynamic ones,
191
- install rubocop-rspec and run:
192
-
193
- rubocop \\
194
- --require rubocop-rspec \\
195
- --only FactoryBot/AttributeDefinedStatically \\
196
- --auto-correct
197
-
198
- MSG
199
190
  end
200
191
  end
201
192
  end