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,33 @@
1
+ module FactoryBot
2
+ class FactoryRunner
3
+ def initialize(name, strategy, traits_and_overrides)
4
+ @name = name
5
+ @strategy = strategy
6
+
7
+ @overrides = traits_and_overrides.extract_options!
8
+ @traits = traits_and_overrides
9
+ end
10
+
11
+ def run(runner_strategy = @strategy, &block)
12
+ factory = FactoryBot.factory_by_name(@name)
13
+
14
+ factory.compile
15
+
16
+ if @traits.any?
17
+ factory = factory.with_traits(@traits)
18
+ end
19
+
20
+ instrumentation_payload = {
21
+ name: @name,
22
+ strategy: runner_strategy,
23
+ traits: @traits,
24
+ overrides: @overrides,
25
+ factory: factory
26
+ }
27
+
28
+ ActiveSupport::Notifications.instrument('factory_bot.run_factory', instrumentation_payload) do
29
+ factory.run(runner_strategy, @overrides, &block)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ module FactoryBot
2
+ class << self
3
+ # An Array of strings specifying locations that should be searched for
4
+ # factory definitions. By default, factory_bot will attempt to require
5
+ # "factories", "test/factories" and "spec/factories". Only the first
6
+ # existing file will be loaded.
7
+ attr_accessor :definition_file_paths
8
+ end
9
+
10
+ self.definition_file_paths = %w(factories test/factories spec/factories)
11
+
12
+ def self.find_definitions
13
+ absolute_definition_file_paths = definition_file_paths.map { |path| File.expand_path(path) }
14
+
15
+ absolute_definition_file_paths.uniq.each do |path|
16
+ load("#{path}.rb") if File.exist?("#{path}.rb")
17
+
18
+ if File.directory? path
19
+ Dir[File.join(path, '**', '*.rb')].sort.each do |file|
20
+ load file
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,97 @@
1
+ module FactoryBot
2
+ class Linter
3
+
4
+ def initialize(factories_to_lint, linting_strategy)
5
+ @factories_to_lint = factories_to_lint
6
+ @linting_method = "lint_#{linting_strategy}"
7
+ @invalid_factories = calculate_invalid_factories
8
+ end
9
+
10
+ def lint!
11
+ if invalid_factories.any?
12
+ raise InvalidFactoryError, error_message
13
+ end
14
+ end
15
+
16
+ attr_reader :factories_to_lint, :invalid_factories
17
+ private :factories_to_lint, :invalid_factories
18
+
19
+ private
20
+
21
+ def calculate_invalid_factories
22
+ factories_to_lint.reduce(Hash.new([])) do |result, factory|
23
+ errors = send(@linting_method, factory)
24
+ result[factory] |= errors unless errors.empty?
25
+ result
26
+ end
27
+ end
28
+
29
+ class FactoryError
30
+ def initialize(wrapped_error, factory)
31
+ @wrapped_error = wrapped_error
32
+ @factory = factory
33
+ end
34
+
35
+ def message
36
+ message = @wrapped_error.message
37
+ "* #{location} - #{message} (#{@wrapped_error.class.name})"
38
+ end
39
+
40
+ def location
41
+ @factory.name
42
+ end
43
+ end
44
+
45
+ class FactoryTraitError < FactoryError
46
+ def initialize(wrapped_error, factory, trait_name)
47
+ super(wrapped_error, factory)
48
+ @trait_name = trait_name
49
+ end
50
+
51
+ def location
52
+ "#{@factory.name}+#{@trait_name}"
53
+ end
54
+ end
55
+
56
+ def lint_factory(factory)
57
+ result = []
58
+ begin
59
+ FactoryBot.create(factory.name)
60
+ rescue => error
61
+ result |= [FactoryError.new(error, factory)]
62
+ end
63
+ result
64
+ end
65
+
66
+ def lint_traits(factory)
67
+ result = []
68
+ factory.definition.defined_traits.map(&:name).each do |trait_name|
69
+ begin
70
+ FactoryBot.create(factory.name, trait_name)
71
+ rescue => error
72
+ result |=
73
+ [FactoryTraitError.new(error, factory, trait_name)]
74
+ end
75
+ end
76
+ result
77
+ end
78
+
79
+ def lint_factory_and_traits(factory)
80
+ errors = lint_factory(factory)
81
+ errors |= lint_traits(factory)
82
+ errors
83
+ end
84
+
85
+ def error_message
86
+ lines = invalid_factories.map do |_factory, exceptions|
87
+ exceptions.map(&:message)
88
+ end.flatten
89
+
90
+ <<-ERROR_MESSAGE.strip
91
+ The following factories are invalid:
92
+
93
+ #{lines.join("\n")}
94
+ ERROR_MESSAGE
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,18 @@
1
+ module FactoryBot
2
+ # @api private
3
+ class NullFactory
4
+ attr_reader :definition
5
+
6
+ def initialize
7
+ @definition = Definition.new(:null_factory)
8
+ end
9
+
10
+ delegate :defined_traits, :callbacks, :attributes, :constructor,
11
+ :to_create, to: :definition
12
+
13
+ def compile; end
14
+ def class_name; end
15
+ def evaluator_class; FactoryBot::Evaluator; end
16
+ def hierarchy_class; FactoryBot::DefinitionHierarchy; end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ module FactoryBot
2
+ # @api private
3
+ class NullObject < ::BasicObject
4
+ def initialize(methods_to_respond_to)
5
+ @methods_to_respond_to = methods_to_respond_to.map(&:to_s)
6
+ end
7
+
8
+ def method_missing(name, *args, &block)
9
+ if respond_to?(name)
10
+ nil
11
+ else
12
+ super
13
+ end
14
+ end
15
+
16
+ def respond_to?(method, include_private=false)
17
+ @methods_to_respond_to.include? method.to_s
18
+ end
19
+
20
+ def respond_to_missing?(*args)
21
+ false
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ module FactoryBot
2
+ class Registry
3
+ include Enumerable
4
+
5
+ attr_reader :name
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ @items = Decorator::ClassKeyHash.new({})
10
+ end
11
+
12
+ def clear
13
+ @items.clear
14
+ end
15
+
16
+ def each(&block)
17
+ @items.values.uniq.each(&block)
18
+ end
19
+
20
+ def find(name)
21
+ if registered?(name)
22
+ @items[name]
23
+ else
24
+ raise ArgumentError, "#{@name} not registered: #{name}"
25
+ end
26
+ end
27
+
28
+ alias :[] :find
29
+
30
+ def register(name, item)
31
+ @items[name] = item
32
+ end
33
+
34
+ def registered?(name)
35
+ @items.key?(name)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,8 @@
1
+ module FactoryBot
2
+ def self.reload
3
+ reset_configuration
4
+ register_default_strategies
5
+ register_default_callbacks
6
+ find_definitions
7
+ end
8
+ end
@@ -0,0 +1,62 @@
1
+ module FactoryBot
2
+
3
+ # Sequences are defined using sequence within a FactoryBot.define block.
4
+ # Sequence values are generated using next.
5
+ # @api private
6
+ class Sequence
7
+ attr_reader :name
8
+
9
+ def initialize(name, *args, &proc)
10
+ @name = name
11
+ @proc = proc
12
+
13
+ options = args.extract_options!
14
+ @value = args.first || 1
15
+ @aliases = options.fetch(:aliases) { [] }
16
+
17
+ if !@value.respond_to?(:peek)
18
+ @value = EnumeratorAdapter.new(@value)
19
+ end
20
+ end
21
+
22
+ def next(scope = nil)
23
+ if @proc && scope
24
+ scope.instance_exec(value, &@proc)
25
+ elsif @proc
26
+ @proc.call(value)
27
+ else
28
+ value
29
+ end
30
+ ensure
31
+ increment_value
32
+ end
33
+
34
+ def names
35
+ [@name] + @aliases
36
+ end
37
+
38
+ private
39
+
40
+ def value
41
+ @value.peek
42
+ end
43
+
44
+ def increment_value
45
+ @value.next
46
+ end
47
+
48
+ class EnumeratorAdapter
49
+ def initialize(value)
50
+ @value = value
51
+ end
52
+
53
+ def peek
54
+ @value
55
+ end
56
+
57
+ def next
58
+ @value = @value.next
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,13 @@
1
+ module FactoryBot
2
+ module Strategy
3
+ class AttributesFor
4
+ def association(runner)
5
+ runner.run(:null)
6
+ end
7
+
8
+ def result(evaluation)
9
+ evaluation.hash
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ module FactoryBot
2
+ module Strategy
3
+ class Build
4
+ def association(runner)
5
+ runner.run
6
+ end
7
+
8
+ def result(evaluation)
9
+ evaluation.object.tap do |instance|
10
+ evaluation.notify(:after_build, instance)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ module FactoryBot
2
+ module Strategy
3
+ class Create
4
+ def association(runner)
5
+ runner.run
6
+ end
7
+
8
+ def result(evaluation)
9
+ evaluation.object.tap do |instance|
10
+ evaluation.notify(:after_build, instance)
11
+ evaluation.notify(:before_create, instance)
12
+ evaluation.create(instance)
13
+ evaluation.notify(:after_create, instance)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module FactoryBot
2
+ module Strategy
3
+ class Null
4
+ def association(runner)
5
+ end
6
+
7
+ def result(evaluation)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,108 @@
1
+ module FactoryBot
2
+ module Strategy
3
+ class Stub
4
+ @@next_id = 1000
5
+
6
+ DISABLED_PERSISTENCE_METHODS = [
7
+ :connection,
8
+ :decrement!,
9
+ :decrement,
10
+ :delete,
11
+ :destroy!,
12
+ :destroy,
13
+ :destroyed?,
14
+ :increment!,
15
+ :increment,
16
+ :reload,
17
+ :save!,
18
+ :save,
19
+ :toggle!,
20
+ :toggle,
21
+ :touch,
22
+ :update!,
23
+ :update,
24
+ :update_attribute,
25
+ :update_attributes!,
26
+ :update_attributes,
27
+ :update_column,
28
+ :update_columns,
29
+ ].freeze
30
+
31
+ def association(runner)
32
+ runner.run(:build_stubbed)
33
+ end
34
+
35
+ def result(evaluation)
36
+ evaluation.object.tap do |instance|
37
+ stub_database_interaction_on_result(instance)
38
+ clear_changed_attributes_on_result(instance)
39
+ evaluation.notify(:after_stub, instance)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def next_id
46
+ @@next_id += 1
47
+ end
48
+
49
+ def stub_database_interaction_on_result(result_instance)
50
+ result_instance.id ||= next_id
51
+
52
+ result_instance.instance_eval do
53
+ def persisted?
54
+ !new_record?
55
+ end
56
+
57
+ def new_record?
58
+ id.nil?
59
+ end
60
+
61
+ DISABLED_PERSISTENCE_METHODS.each do |write_method|
62
+ 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
+ end
65
+ end
66
+ end
67
+
68
+ created_at_missing_default = result_instance.respond_to?(:created_at) && !result_instance.created_at
69
+ result_instance_missing_created_at = !result_instance.respond_to?(:created_at)
70
+
71
+ if created_at_missing_default || result_instance_missing_created_at
72
+ result_instance.instance_eval do
73
+ def created_at
74
+ @created_at ||= Time.now.in_time_zone
75
+ end
76
+ end
77
+ end
78
+
79
+ has_updated_at = result_instance.respond_to?(:updated_at)
80
+ updated_at_no_default = has_updated_at && !result_instance.updated_at
81
+ result_instance_missing_updated_at = !has_updated_at
82
+
83
+ if updated_at_no_default || result_instance_missing_updated_at
84
+ result_instance.instance_eval do
85
+ def updated_at
86
+ @updated_at ||= Time.current
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def clear_changed_attributes_on_result(result_instance)
93
+ unless result_instance.respond_to?(:clear_changes_information)
94
+ result_instance.extend ActiveModelDirtyBackport
95
+ end
96
+
97
+ result_instance.clear_changes_information
98
+ end
99
+ end
100
+
101
+ module ActiveModelDirtyBackport
102
+ def clear_changes_information
103
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
104
+ @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
105
+ end
106
+ end
107
+ end
108
+ end