factory_bot 4.11.1 → 5.0.0.rc1

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 (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
@@ -11,12 +11,12 @@ module FactoryBot
11
11
  # Raised when attempting to register a sequence from a dynamic attribute block
12
12
  class SequenceAbuseError < RuntimeError; end
13
13
 
14
- # Raised when defining an invalid attribute:
15
- # * Defining an attribute which has a name ending in "="
16
- # * Defining an attribute with both a static and lazy value
17
- # * Defining an attribute twice in the same factory
14
+ # Raised when defining an attribute twice in the same factory
18
15
  class AttributeDefinitionError < RuntimeError; end
19
16
 
17
+ # Raised when attempting to pass a block to an association definition
18
+ class AssociationDefinitionError < RuntimeError; end
19
+
20
20
  # Raised when a method is defined in a factory or trait with arguments
21
21
  class MethodDefinitionError < RuntimeError; end
22
22
 
@@ -1,4 +1,4 @@
1
- require 'observer'
1
+ require "observer"
2
2
 
3
3
  module FactoryBot
4
4
  class Evaluation
@@ -1,5 +1,5 @@
1
- require 'active_support/core_ext/hash/except'
2
- require 'active_support/core_ext/class/attribute'
1
+ require "active_support/core_ext/hash/except"
2
+ require "active_support/core_ext/class/attribute"
3
3
 
4
4
  module FactoryBot
5
5
  # @api private
@@ -37,7 +37,7 @@ module FactoryBot
37
37
  @instance = object_instance
38
38
  end
39
39
 
40
- def method_missing(method_name, *args, &block)
40
+ def method_missing(method_name, *args, &block) # rubocop:disable Style/MethodMissing
41
41
  if @instance.respond_to?(method_name)
42
42
  @instance.send(method_name, *args, &block)
43
43
  else
@@ -45,7 +45,7 @@ module FactoryBot
45
45
  end
46
46
  end
47
47
 
48
- def respond_to_missing?(method_name, include_private = false)
48
+ def respond_to_missing?(method_name, _include_private = false)
49
49
  @instance.respond_to?(method_name) || SyntaxRunner.new.respond_to?(method_name)
50
50
  end
51
51
 
@@ -1,5 +1,5 @@
1
- require 'active_support/core_ext/hash/keys'
2
- require 'active_support/inflector'
1
+ require "active_support/core_ext/hash/keys"
2
+ require "active_support/inflector"
3
3
 
4
4
  module FactoryBot
5
5
  # @api private
@@ -21,10 +21,10 @@ module FactoryBot
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)
@@ -91,7 +91,7 @@ module FactoryBot
91
91
  end
92
92
 
93
93
  def with_traits(traits)
94
- self.clone.tap do |factory_with_traits|
94
+ clone.tap do |factory_with_traits|
95
95
  factory_with_traits.append_traits traits
96
96
  end
97
97
  end
@@ -5,7 +5,7 @@ module FactoryBot
5
5
  @strategy = strategy
6
6
 
7
7
  @overrides = traits_and_overrides.extract_options!
8
- @traits = traits_and_overrides
8
+ @traits = traits_and_overrides.map(&:to_sym)
9
9
  end
10
10
 
11
11
  def run(runner_strategy = @strategy, &block)
@@ -22,10 +22,10 @@ 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
- ActiveSupport::Notifications.instrument('factory_bot.run_factory', instrumentation_payload) do
28
+ ActiveSupport::Notifications.instrument("factory_bot.run_factory", instrumentation_payload) do
29
29
  factory.run(runner_strategy, @overrides, &block)
30
30
  end
31
31
  end
@@ -16,7 +16,7 @@ module FactoryBot
16
16
  load("#{path}.rb") if File.exist?("#{path}.rb")
17
17
 
18
18
  if File.directory? path
19
- Dir[File.join(path, '**', '*.rb')].sort.each do |file|
19
+ Dir[File.join(path, "**", "*.rb")].sort.each do |file|
20
20
  load file
21
21
  end
22
22
  end
@@ -1,10 +1,10 @@
1
1
  module FactoryBot
2
2
  class Linter
3
-
4
- def initialize(factories, linting_strategy, factory_strategy = :create)
3
+ def initialize(factories, strategy: :create, traits: false, verbose: false)
5
4
  @factories_to_lint = factories
6
- @linting_method = "lint_#{linting_strategy}"
7
- @factory_strategy = factory_strategy
5
+ @factory_strategy = strategy
6
+ @traits = traits
7
+ @verbose = verbose
8
8
  @invalid_factories = calculate_invalid_factories
9
9
  end
10
10
 
@@ -20,7 +20,7 @@ module FactoryBot
20
20
 
21
21
  def calculate_invalid_factories
22
22
  factories_to_lint.reduce(Hash.new([])) do |result, factory|
23
- errors = send(@linting_method, factory)
23
+ errors = lint(factory)
24
24
  result[factory] |= errors unless errors.empty?
25
25
  result
26
26
  end
@@ -37,6 +37,13 @@ module FactoryBot
37
37
  "* #{location} - #{message} (#{@wrapped_error.class.name})"
38
38
  end
39
39
 
40
+ def verbose_message
41
+ <<~MESSAGE
42
+ #{message}
43
+ #{@wrapped_error.backtrace.join("\n ")}
44
+ MESSAGE
45
+ end
46
+
40
47
  def location
41
48
  @factory.name
42
49
  end
@@ -53,11 +60,19 @@ module FactoryBot
53
60
  end
54
61
  end
55
62
 
63
+ def lint(factory)
64
+ if @traits
65
+ lint_factory(factory) + lint_traits(factory)
66
+ else
67
+ lint_factory(factory)
68
+ end
69
+ end
70
+
56
71
  def lint_factory(factory)
57
72
  result = []
58
73
  begin
59
74
  FactoryBot.public_send(factory_strategy, factory.name)
60
- rescue => error
75
+ rescue StandardError => error
61
76
  result |= [FactoryError.new(error, factory)]
62
77
  end
63
78
  result
@@ -68,30 +83,32 @@ module FactoryBot
68
83
  factory.definition.defined_traits.map(&:name).each do |trait_name|
69
84
  begin
70
85
  FactoryBot.public_send(factory_strategy, factory.name, trait_name)
71
- rescue => error
86
+ rescue StandardError => error
72
87
  result |=
73
- [FactoryTraitError.new(error, factory, trait_name)]
88
+ [FactoryTraitError.new(error, factory, trait_name)]
74
89
  end
75
90
  end
76
91
  result
77
92
  end
78
93
 
79
- def lint_factory_and_traits(factory)
80
- errors = lint_factory(factory)
81
- errors |= lint_traits(factory)
82
- errors
83
- end
84
-
85
94
  def error_message
86
95
  lines = invalid_factories.map do |_factory, exceptions|
87
- exceptions.map(&:message)
96
+ exceptions.map(&error_message_type)
88
97
  end.flatten
89
98
 
90
- <<-ERROR_MESSAGE.strip
91
- The following factories are invalid:
99
+ <<~ERROR_MESSAGE.strip
100
+ The following factories are invalid:
92
101
 
93
- #{lines.join("\n")}
102
+ #{lines.join("\n")}
94
103
  ERROR_MESSAGE
95
104
  end
105
+
106
+ def error_message_type
107
+ if @verbose
108
+ :verbose_message
109
+ else
110
+ :message
111
+ end
112
+ end
96
113
  end
97
114
  end
@@ -11,8 +11,11 @@ module FactoryBot
11
11
  :to_create, to: :definition
12
12
 
13
13
  def compile; end
14
+
14
15
  def class_name; end
16
+
15
17
  def evaluator_class; FactoryBot::Evaluator; end
18
+
16
19
  def hierarchy_class; FactoryBot::DefinitionHierarchy; end
17
20
  end
18
21
  end
@@ -13,11 +13,11 @@ module FactoryBot
13
13
  end
14
14
  end
15
15
 
16
- def respond_to?(method, include_private=false)
16
+ def respond_to?(method, _include_private = false)
17
17
  @methods_to_respond_to.include? method.to_s
18
18
  end
19
19
 
20
- def respond_to_missing?(*args)
20
+ def respond_to_missing?(*)
21
21
  false
22
22
  end
23
23
  end
@@ -1,3 +1,5 @@
1
+ require "active_support/core_ext/hash/indifferent_access"
2
+
1
3
  module FactoryBot
2
4
  class Registry
3
5
  include Enumerable
@@ -6,7 +8,7 @@ module FactoryBot
6
8
 
7
9
  def initialize(name)
8
10
  @name = name
9
- @items = Decorator::ClassKeyHash.new({})
11
+ @items = ActiveSupport::HashWithIndifferentAccess.new
10
12
  end
11
13
 
12
14
  def clear
@@ -18,11 +20,9 @@ module FactoryBot
18
20
  end
19
21
 
20
22
  def find(name)
21
- if registered?(name)
22
- @items[name]
23
- else
24
- raise ArgumentError, "#{@name} not registered: #{name}"
25
- end
23
+ @items.fetch(name)
24
+ rescue KeyError => key_error
25
+ raise key_error_with_custom_message(key_error)
26
26
  end
27
27
 
28
28
  alias :[] :find
@@ -34,5 +34,14 @@ module FactoryBot
34
34
  def registered?(name)
35
35
  @items.key?(name)
36
36
  end
37
+
38
+ private
39
+
40
+ def key_error_with_custom_message(key_error)
41
+ message = key_error.message.sub("key not found", "#{@name} not registered")
42
+ error = KeyError.new(message)
43
+ error.set_backtrace(key_error.backtrace)
44
+ error
45
+ end
37
46
  end
38
47
  end
@@ -1,5 +1,4 @@
1
1
  module FactoryBot
2
-
3
2
  # Sequences are defined using sequence within a FactoryBot.define block.
4
3
  # Sequence values are generated using next.
5
4
  # @api private
@@ -1,11 +1,9 @@
1
1
  module FactoryBot
2
2
  module Strategy
3
3
  class Null
4
- def association(runner)
5
- end
4
+ def association(runner); end
6
5
 
7
- def result(evaluation)
8
- end
6
+ def result(evaluation); end
9
7
  end
10
8
  end
11
9
  end
@@ -31,7 +31,8 @@ module FactoryBot
31
31
  def result(evaluation)
32
32
  evaluation.object.tap do |instance|
33
33
  stub_database_interaction_on_result(instance)
34
- clear_changed_attributes_on_result(instance)
34
+ set_timestamps(instance)
35
+ clear_changes_information(instance)
35
36
  evaluation.notify(:after_stub, instance)
36
37
  end
37
38
  end
@@ -60,46 +61,39 @@ module FactoryBot
60
61
 
61
62
  DISABLED_PERSISTENCE_METHODS.each do |write_method|
62
63
  define_singleton_method(write_method) do |*args|
63
- raise "stubbed models are not allowed to access the database - #{self.class}##{write_method}(#{args.join(",")})"
64
+ raise "stubbed models are not allowed to access the database - "\
65
+ "#{self.class}##{write_method}(#{args.join(',')})"
64
66
  end
65
67
  end
66
68
  end
69
+ end
67
70
 
68
- created_at_missing_default = result_instance.respond_to?(:created_at) && !result_instance.created_at
69
-
70
- if created_at_missing_default
71
- result_instance.instance_eval do
72
- def created_at
73
- @created_at ||= Time.now.in_time_zone
74
- end
75
- end
71
+ def clear_changes_information(result_instance)
72
+ if result_instance.respond_to?(:clear_changes_information)
73
+ result_instance.clear_changes_information
76
74
  end
75
+ end
77
76
 
78
- has_updated_at = result_instance.respond_to?(:updated_at)
79
- updated_at_no_default = has_updated_at && !result_instance.updated_at
80
-
81
- if updated_at_no_default
82
- result_instance.instance_eval do
83
- def updated_at
84
- @updated_at ||= Time.current
85
- end
86
- end
77
+ def set_timestamps(result_instance)
78
+ if missing_created_at?(result_instance)
79
+ result_instance.created_at = Time.current
87
80
  end
88
- end
89
81
 
90
- def clear_changed_attributes_on_result(result_instance)
91
- unless result_instance.respond_to?(:clear_changes_information)
92
- result_instance.extend ActiveModelDirtyBackport
82
+ if missing_updated_at?(result_instance)
83
+ result_instance.updated_at = Time.current
93
84
  end
85
+ end
94
86
 
95
- result_instance.clear_changes_information
87
+ def missing_created_at?(result_instance)
88
+ result_instance.respond_to?(:created_at) &&
89
+ result_instance.respond_to?(:created_at=) &&
90
+ result_instance.created_at.blank?
96
91
  end
97
- end
98
92
 
99
- module ActiveModelDirtyBackport
100
- def clear_changes_information
101
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
102
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
93
+ def missing_updated_at?(result_instance)
94
+ result_instance.respond_to?(:updated_at) &&
95
+ result_instance.respond_to?(:updated_at=) &&
96
+ result_instance.updated_at.blank?
103
97
  end
104
98
  end
105
99
  end
@@ -29,7 +29,7 @@ module FactoryBot
29
29
  raise ArgumentError, "count missing for #{strategy_name}_list"
30
30
  end
31
31
 
32
- amount.times.map { send(strategy_name, name, *traits_and_overrides, &block) }
32
+ Array.new(amount) { send(strategy_name, name, *traits_and_overrides, &block) }
33
33
  end
34
34
  end
35
35
 
@@ -37,7 +37,7 @@ module FactoryBot
37
37
  strategy_name = @strategy_name
38
38
 
39
39
  define_syntax_method("#{strategy_name}_pair") do |name, *traits_and_overrides, &block|
40
- 2.times.map { send(strategy_name, name, *traits_and_overrides, &block) }
40
+ Array.new(2) { send(strategy_name, name, *traits_and_overrides, &block) }
41
41
  end
42
42
  end
43
43
 
@@ -1,5 +1,5 @@
1
- require 'factory_bot/syntax/methods'
2
- require 'factory_bot/syntax/default'
1
+ require "factory_bot/syntax/methods"
2
+ require "factory_bot/syntax/default"
3
3
 
4
4
  module FactoryBot
5
5
  module Syntax
@@ -59,7 +59,7 @@ module FactoryBot
59
59
  end
60
60
 
61
61
  class ModifyDSL
62
- def factory(name, options = {}, &block)
62
+ def factory(name, _options = {}, &block)
63
63
  factory = FactoryBot.factory_by_name(name)
64
64
  proxy = FactoryBot::DefinitionProxy.new(factory.definition.overridable)
65
65
  proxy.instance_eval(&block)
@@ -4,7 +4,7 @@ module FactoryBot
4
4
  attr_reader :name, :definition
5
5
 
6
6
  def initialize(name, &block)
7
- @name = name
7
+ @name = name.to_sym
8
8
  @block = block
9
9
  @definition = Definition.new(@name)
10
10
 
@@ -25,6 +25,7 @@ module FactoryBot
25
25
  end
26
26
 
27
27
  protected
28
+
28
29
  attr_reader :block
29
30
  end
30
31
  end