factory_bot 1.0.0.alpha

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 (86) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +9 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +2 -0
  5. data/.simplecov +4 -0
  6. data/.travis.yml +41 -0
  7. data/.yardopts +5 -0
  8. data/Appraisals +19 -0
  9. data/CONTRIBUTING.md +60 -0
  10. data/GETTING_STARTED.md +1387 -0
  11. data/Gemfile +8 -0
  12. data/Gemfile.lock +123 -0
  13. data/LICENSE +19 -0
  14. data/NAME.md +11 -0
  15. data/NEWS +285 -0
  16. data/README.md +102 -0
  17. data/Rakefile +36 -0
  18. data/cucumber.yml +1 -0
  19. data/factory_bot.gemspec +38 -0
  20. data/factory_girl.gemspec +41 -0
  21. data/gemfiles/3.2.gemfile +10 -0
  22. data/gemfiles/3.2.gemfile.lock +112 -0
  23. data/gemfiles/4.0.gemfile +10 -0
  24. data/gemfiles/4.0.gemfile.lock +112 -0
  25. data/gemfiles/4.1.gemfile +10 -0
  26. data/gemfiles/4.1.gemfile.lock +111 -0
  27. data/gemfiles/4.2.gemfile +10 -0
  28. data/gemfiles/4.2.gemfile.lock +111 -0
  29. data/gemfiles/5.0.gemfile +10 -0
  30. data/gemfiles/5.0.gemfile.lock +110 -0
  31. data/lib/factory_bot.rb +154 -0
  32. data/lib/factory_bot/aliases.rb +18 -0
  33. data/lib/factory_bot/attribute.rb +62 -0
  34. data/lib/factory_bot/attribute/association.rb +27 -0
  35. data/lib/factory_bot/attribute/dynamic.rb +24 -0
  36. data/lib/factory_bot/attribute/sequence.rb +16 -0
  37. data/lib/factory_bot/attribute/static.rb +16 -0
  38. data/lib/factory_bot/attribute_assigner.rb +97 -0
  39. data/lib/factory_bot/attribute_list.rb +67 -0
  40. data/lib/factory_bot/callback.rb +40 -0
  41. data/lib/factory_bot/callbacks_observer.rb +21 -0
  42. data/lib/factory_bot/configuration.rb +37 -0
  43. data/lib/factory_bot/declaration.rb +23 -0
  44. data/lib/factory_bot/declaration/association.rb +28 -0
  45. data/lib/factory_bot/declaration/dynamic.rb +26 -0
  46. data/lib/factory_bot/declaration/implicit.rb +33 -0
  47. data/lib/factory_bot/declaration/static.rb +26 -0
  48. data/lib/factory_bot/declaration_list.rb +49 -0
  49. data/lib/factory_bot/decorator.rb +21 -0
  50. data/lib/factory_bot/decorator/attribute_hash.rb +16 -0
  51. data/lib/factory_bot/decorator/class_key_hash.rb +28 -0
  52. data/lib/factory_bot/decorator/disallows_duplicates_registry.rb +13 -0
  53. data/lib/factory_bot/decorator/invocation_tracker.rb +19 -0
  54. data/lib/factory_bot/decorator/new_constructor.rb +12 -0
  55. data/lib/factory_bot/definition.rb +136 -0
  56. data/lib/factory_bot/definition_hierarchy.rb +48 -0
  57. data/lib/factory_bot/definition_proxy.rb +174 -0
  58. data/lib/factory_bot/errors.rb +25 -0
  59. data/lib/factory_bot/evaluation.rb +23 -0
  60. data/lib/factory_bot/evaluator.rb +82 -0
  61. data/lib/factory_bot/evaluator_class_definer.rb +20 -0
  62. data/lib/factory_bot/factory.rb +162 -0
  63. data/lib/factory_bot/factory_runner.rb +33 -0
  64. data/lib/factory_bot/find_definitions.rb +25 -0
  65. data/lib/factory_bot/linter.rb +97 -0
  66. data/lib/factory_bot/null_factory.rb +18 -0
  67. data/lib/factory_bot/null_object.rb +24 -0
  68. data/lib/factory_bot/registry.rb +38 -0
  69. data/lib/factory_bot/reload.rb +8 -0
  70. data/lib/factory_bot/sequence.rb +62 -0
  71. data/lib/factory_bot/strategy/attributes_for.rb +13 -0
  72. data/lib/factory_bot/strategy/build.rb +15 -0
  73. data/lib/factory_bot/strategy/create.rb +18 -0
  74. data/lib/factory_bot/strategy/null.rb +11 -0
  75. data/lib/factory_bot/strategy/stub.rb +108 -0
  76. data/lib/factory_bot/strategy_calculator.rb +26 -0
  77. data/lib/factory_bot/strategy_syntax_method_registrar.rb +54 -0
  78. data/lib/factory_bot/syntax.rb +7 -0
  79. data/lib/factory_bot/syntax/default.rb +76 -0
  80. data/lib/factory_bot/syntax/methods.rb +111 -0
  81. data/lib/factory_bot/syntax_runner.rb +6 -0
  82. data/lib/factory_bot/trait.rb +30 -0
  83. data/lib/factory_bot/version.rb +3 -0
  84. data/lib/factory_girl.rb +5 -0
  85. data/lib/factory_girl/version.rb +1 -0
  86. metadata +302 -0
@@ -0,0 +1,174 @@
1
+ module FactoryBot
2
+ class DefinitionProxy
3
+ UNPROXIED_METHODS = %w(__send__ __id__ nil? send object_id extend instance_eval initialize block_given? raise caller method)
4
+
5
+ (instance_methods + private_instance_methods).each do |method|
6
+ undef_method(method) unless UNPROXIED_METHODS.include?(method.to_s)
7
+ end
8
+
9
+ delegate :before, :after, :callback, to: :@definition
10
+
11
+ attr_reader :child_factories
12
+
13
+ def initialize(definition, ignore = false)
14
+ @definition = definition
15
+ @ignore = ignore
16
+ @child_factories = []
17
+ end
18
+
19
+ def singleton_method_added(name)
20
+ message = "Defining methods in blocks (trait or factory) is not supported (#{name})"
21
+ raise FactoryBot::MethodDefinitionError, message
22
+ end
23
+
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
30
+ # attribute is overridden for a specific instance.
31
+ #
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
+ # Arguments:
37
+ # * name: +Symbol+ or +String+
38
+ # The name of this attribute. This will be assigned using "name=" for
39
+ # 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
+ Declaration::Static.new(name, value, @ignore)
49
+ end
50
+
51
+ @definition.declare_attribute(declaration)
52
+ end
53
+
54
+ def ignore(&block)
55
+ ActiveSupport::Deprecation.warn "`#ignore` is deprecated and will be "\
56
+ "removed in 5.0. Please use `#transient` instead."
57
+ transient(&block)
58
+ end
59
+
60
+ def transient(&block)
61
+ proxy = DefinitionProxy.new(@definition, true)
62
+ proxy.instance_eval(&block)
63
+ end
64
+
65
+ # Calls add_attribute using the missing method name as the name of the
66
+ # attribute, so that:
67
+ #
68
+ # factory :user do
69
+ # name 'Billy Idol'
70
+ # end
71
+ #
72
+ # and:
73
+ #
74
+ # factory :user do
75
+ # add_attribute :name, 'Billy Idol'
76
+ # end
77
+ #
78
+ # are equivalent.
79
+ #
80
+ # If no argument or block is given, factory_bot will look for a sequence
81
+ # or association with the same name. This means that:
82
+ #
83
+ # factory :user do
84
+ # email { create(:email) }
85
+ # association :account
86
+ # end
87
+ #
88
+ # and:
89
+ #
90
+ # factory :user do
91
+ # email
92
+ # account
93
+ # end
94
+ #
95
+ # are equivalent.
96
+ def method_missing(name, *args, &block)
97
+ if args.empty? && block.nil?
98
+ @definition.declare_attribute(Declaration::Implicit.new(name, @definition, @ignore))
99
+ elsif args.first.respond_to?(:has_key?) && args.first.has_key?(:factory)
100
+ association(name, *args)
101
+ else
102
+ add_attribute(name, *args, &block)
103
+ end
104
+ end
105
+
106
+ # Adds an attribute that will have unique values generated by a sequence with
107
+ # a specified format.
108
+ #
109
+ # The result of:
110
+ # factory :user do
111
+ # sequence(:email) { |n| "person#{n}@example.com" }
112
+ # end
113
+ #
114
+ # Is equal to:
115
+ # sequence(:email) { |n| "person#{n}@example.com" }
116
+ #
117
+ # factory :user do
118
+ # email { FactoryBot.generate(:email) }
119
+ # end
120
+ #
121
+ # Except that no globally available sequence will be defined.
122
+ def sequence(name, *args, &block)
123
+ sequence = Sequence.new(name, *args, &block)
124
+ add_attribute(name) { increment_sequence(sequence) }
125
+ end
126
+
127
+ # Adds an attribute that builds an association. The associated instance will
128
+ # be built using the same build strategy as the parent instance.
129
+ #
130
+ # Example:
131
+ # factory :user do
132
+ # name 'Joey'
133
+ # end
134
+ #
135
+ # factory :post do
136
+ # association :author, factory: :user
137
+ # end
138
+ #
139
+ # Arguments:
140
+ # * name: +Symbol+
141
+ # The name of this attribute.
142
+ # * options: +Hash+
143
+ #
144
+ # Options:
145
+ # * factory: +Symbol+ or +String+
146
+ # The name of the factory to use when building the associated instance.
147
+ # If no name is given, the name of the attribute is assumed to be the
148
+ # name of the factory. For example, a "user" association will by
149
+ # default use the "user" factory.
150
+ def association(name, *options)
151
+ @definition.declare_attribute(Declaration::Association.new(name, *options))
152
+ end
153
+
154
+ def to_create(&block)
155
+ @definition.to_create(&block)
156
+ end
157
+
158
+ def skip_create
159
+ @definition.skip_create
160
+ end
161
+
162
+ def factory(name, options = {}, &block)
163
+ @child_factories << [name, options, block]
164
+ end
165
+
166
+ def trait(name, &block)
167
+ @definition.define_trait(Trait.new(name, &block))
168
+ end
169
+
170
+ def initialize_with(&block)
171
+ @definition.define_constructor(&block)
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,25 @@
1
+ module FactoryBot
2
+ # Raised when a factory is defined that attempts to instantiate itself.
3
+ class AssociationDefinitionError < RuntimeError; end
4
+
5
+ # Raised when a callback is defined that has an invalid name
6
+ class InvalidCallbackNameError < RuntimeError; end
7
+
8
+ # Raised when a factory is defined with the same name as a previously-defined factory.
9
+ class DuplicateDefinitionError < RuntimeError; end
10
+
11
+ # Raised when attempting to register a sequence from a dynamic attribute block
12
+ class SequenceAbuseError < RuntimeError; end
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
18
+ class AttributeDefinitionError < RuntimeError; end
19
+
20
+ # Raised when a method is defined in a factory or trait with arguments
21
+ class MethodDefinitionError < RuntimeError; end
22
+
23
+ # Raised when any factory is considered invalid
24
+ class InvalidFactoryError < RuntimeError; end
25
+ end
@@ -0,0 +1,23 @@
1
+ require 'observer'
2
+
3
+ module FactoryBot
4
+ class Evaluation
5
+ include Observable
6
+
7
+ def initialize(attribute_assigner, to_create)
8
+ @attribute_assigner = attribute_assigner
9
+ @to_create = to_create
10
+ end
11
+
12
+ delegate :object, :hash, to: :@attribute_assigner
13
+
14
+ def create(result_instance)
15
+ @to_create[result_instance]
16
+ end
17
+
18
+ def notify(name, result_instance)
19
+ changed
20
+ notify_observers(name, result_instance)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,82 @@
1
+ require 'active_support/core_ext/hash/except'
2
+ require 'active_support/core_ext/class/attribute'
3
+
4
+ module FactoryBot
5
+ # @api private
6
+ class Evaluator
7
+ class_attribute :attribute_lists
8
+
9
+ private_instance_methods.each do |method|
10
+ undef_method(method) unless method =~ /^__|initialize/
11
+ end
12
+
13
+ def initialize(build_strategy, overrides = {})
14
+ @build_strategy = build_strategy
15
+ @overrides = overrides
16
+ @cached_attributes = overrides
17
+ @instance = nil
18
+
19
+ @overrides.each do |name, value|
20
+ singleton_class.define_attribute(name) { value }
21
+ end
22
+ end
23
+
24
+ def association(factory_name, *traits_and_overrides)
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
29
+
30
+ traits_and_overrides += [overrides.except(:strategy)]
31
+
32
+ runner = FactoryRunner.new(factory_name, strategy_override, traits_and_overrides)
33
+ @build_strategy.association(runner)
34
+ end
35
+
36
+ def instance=(object_instance)
37
+ @instance = object_instance
38
+ end
39
+
40
+ def method_missing(method_name, *args, &block)
41
+ if @instance.respond_to?(method_name)
42
+ @instance.send(method_name, *args, &block)
43
+ else
44
+ SyntaxRunner.new.send(method_name, *args, &block)
45
+ end
46
+ end
47
+
48
+ def respond_to_missing?(method_name, include_private = false)
49
+ @instance.respond_to?(method_name) || SyntaxRunner.new.respond_to?(method_name)
50
+ end
51
+
52
+ def __override_names__
53
+ @overrides.keys
54
+ end
55
+
56
+ def increment_sequence(sequence)
57
+ sequence.next(self)
58
+ end
59
+
60
+ def self.attribute_list
61
+ AttributeList.new.tap do |list|
62
+ attribute_lists.each do |attribute_list|
63
+ list.apply_attributes attribute_list.to_a
64
+ end
65
+ end
66
+ end
67
+
68
+ def self.define_attribute(name, &block)
69
+ if method_defined?(name) || private_method_defined?(name)
70
+ undef_method(name)
71
+ end
72
+
73
+ define_method(name) do
74
+ if @cached_attributes.key?(name)
75
+ @cached_attributes[name]
76
+ else
77
+ @cached_attributes[name] = instance_exec(&block)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,20 @@
1
+ module FactoryBot
2
+ # @api private
3
+ class EvaluatorClassDefiner
4
+ def initialize(attributes, parent_class)
5
+ @parent_class = parent_class
6
+ @attributes = attributes
7
+
8
+ attributes.each do |attribute|
9
+ evaluator_class.define_attribute(attribute.name, &attribute.to_proc)
10
+ end
11
+ end
12
+
13
+ def evaluator_class
14
+ @evaluator_class ||= Class.new(@parent_class).tap do |klass|
15
+ klass.attribute_lists ||= []
16
+ klass.attribute_lists += [@attributes]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,162 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+ require 'active_support/inflector'
3
+
4
+ module FactoryBot
5
+ # @api private
6
+ class Factory
7
+ attr_reader :name, :definition
8
+
9
+ def initialize(name, options = {})
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
17
+ end
18
+
19
+ delegate :add_callback, :declare_attribute, :to_create, :define_trait, :constructor,
20
+ :defined_traits, :inherit_traits, :append_traits, to: :@definition
21
+
22
+ def build_class
23
+ @build_class ||= if class_name.is_a? Class
24
+ class_name
25
+ else
26
+ class_name.to_s.camelize.constantize
27
+ end
28
+ end
29
+
30
+ def run(build_strategy, overrides, &block)
31
+ block ||= ->(result) { result }
32
+ compile
33
+
34
+ strategy = StrategyCalculator.new(build_strategy).strategy.new
35
+
36
+ evaluator = evaluator_class.new(strategy, overrides.symbolize_keys)
37
+ attribute_assigner = AttributeAssigner.new(evaluator, build_class, &compiled_constructor)
38
+
39
+ evaluation = Evaluation.new(attribute_assigner, compiled_to_create)
40
+ evaluation.add_observer(CallbacksObserver.new(callbacks, evaluator))
41
+
42
+ strategy.result(evaluation).tap(&block)
43
+ end
44
+
45
+ def human_names
46
+ names.map { |name| name.to_s.humanize.downcase }
47
+ end
48
+
49
+ def associations
50
+ evaluator_class.attribute_list.associations
51
+ end
52
+
53
+ # Names for this factory, including aliases.
54
+ #
55
+ # Example:
56
+ #
57
+ # factory :user, aliases: [:author] do
58
+ # # ...
59
+ # end
60
+ #
61
+ # FactoryBot.create(:author).class
62
+ # # => User
63
+ #
64
+ # Because an attribute defined without a value or block will build an
65
+ # association with the same name, this allows associations to be defined
66
+ # without factories, such as:
67
+ #
68
+ # factory :user, aliases: [:author] do
69
+ # # ...
70
+ # end
71
+ #
72
+ # factory :post do
73
+ # author
74
+ # end
75
+ #
76
+ # FactoryBot.create(:post).author.class
77
+ # # => User
78
+ def names
79
+ [name] + @aliases
80
+ end
81
+
82
+ def compile
83
+ unless @compiled
84
+ parent.compile
85
+ parent.defined_traits.each { |trait| define_trait(trait) }
86
+ @definition.compile
87
+ build_hierarchy
88
+ @compiled = true
89
+ end
90
+ end
91
+
92
+ def with_traits(traits)
93
+ self.clone.tap do |factory_with_traits|
94
+ factory_with_traits.append_traits traits
95
+ end
96
+ end
97
+
98
+ protected
99
+
100
+ def class_name
101
+ @class_name || parent.class_name || name
102
+ end
103
+
104
+ def evaluator_class
105
+ @evaluator_class ||= EvaluatorClassDefiner.new(attributes, parent.evaluator_class).evaluator_class
106
+ end
107
+
108
+ def attributes
109
+ compile
110
+ AttributeList.new(@name).tap do |list|
111
+ list.apply_attributes definition.attributes
112
+ end
113
+ end
114
+
115
+ def hierarchy_class
116
+ @hierarchy_class ||= Class.new(parent.hierarchy_class)
117
+ end
118
+
119
+ def hierarchy_instance
120
+ @hierarchy_instance ||= hierarchy_class.new
121
+ end
122
+
123
+ def build_hierarchy
124
+ hierarchy_class.build_from_definition definition
125
+ end
126
+
127
+ def callbacks
128
+ hierarchy_instance.callbacks
129
+ end
130
+
131
+ def compiled_to_create
132
+ hierarchy_instance.to_create
133
+ end
134
+
135
+ def compiled_constructor
136
+ hierarchy_instance.constructor
137
+ end
138
+
139
+ private
140
+
141
+ def assert_valid_options(options)
142
+ options.assert_valid_keys(:class, :parent, :aliases, :traits)
143
+ end
144
+
145
+ def parent
146
+ if @parent
147
+ FactoryBot.factory_by_name(@parent)
148
+ else
149
+ NullFactory.new
150
+ end
151
+ end
152
+
153
+ def initialize_copy(source)
154
+ super
155
+ @definition = @definition.clone
156
+ @evaluator_class = nil
157
+ @hierarchy_class = nil
158
+ @hierarchy_instance = nil
159
+ @compiled = false
160
+ end
161
+ end
162
+ end