factory_bot 1.0.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +9 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.simplecov +4 -0
- data/.travis.yml +41 -0
- data/.yardopts +5 -0
- data/Appraisals +19 -0
- data/CONTRIBUTING.md +60 -0
- data/GETTING_STARTED.md +1387 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +123 -0
- data/LICENSE +19 -0
- data/NAME.md +11 -0
- data/NEWS +285 -0
- data/README.md +102 -0
- data/Rakefile +36 -0
- data/cucumber.yml +1 -0
- data/factory_bot.gemspec +38 -0
- data/factory_girl.gemspec +41 -0
- data/gemfiles/3.2.gemfile +10 -0
- data/gemfiles/3.2.gemfile.lock +112 -0
- data/gemfiles/4.0.gemfile +10 -0
- data/gemfiles/4.0.gemfile.lock +112 -0
- data/gemfiles/4.1.gemfile +10 -0
- data/gemfiles/4.1.gemfile.lock +111 -0
- data/gemfiles/4.2.gemfile +10 -0
- data/gemfiles/4.2.gemfile.lock +111 -0
- data/gemfiles/5.0.gemfile +10 -0
- data/gemfiles/5.0.gemfile.lock +110 -0
- data/lib/factory_bot.rb +154 -0
- data/lib/factory_bot/aliases.rb +18 -0
- data/lib/factory_bot/attribute.rb +62 -0
- data/lib/factory_bot/attribute/association.rb +27 -0
- data/lib/factory_bot/attribute/dynamic.rb +24 -0
- data/lib/factory_bot/attribute/sequence.rb +16 -0
- data/lib/factory_bot/attribute/static.rb +16 -0
- data/lib/factory_bot/attribute_assigner.rb +97 -0
- data/lib/factory_bot/attribute_list.rb +67 -0
- data/lib/factory_bot/callback.rb +40 -0
- data/lib/factory_bot/callbacks_observer.rb +21 -0
- data/lib/factory_bot/configuration.rb +37 -0
- data/lib/factory_bot/declaration.rb +23 -0
- data/lib/factory_bot/declaration/association.rb +28 -0
- data/lib/factory_bot/declaration/dynamic.rb +26 -0
- data/lib/factory_bot/declaration/implicit.rb +33 -0
- data/lib/factory_bot/declaration/static.rb +26 -0
- data/lib/factory_bot/declaration_list.rb +49 -0
- data/lib/factory_bot/decorator.rb +21 -0
- data/lib/factory_bot/decorator/attribute_hash.rb +16 -0
- data/lib/factory_bot/decorator/class_key_hash.rb +28 -0
- data/lib/factory_bot/decorator/disallows_duplicates_registry.rb +13 -0
- data/lib/factory_bot/decorator/invocation_tracker.rb +19 -0
- data/lib/factory_bot/decorator/new_constructor.rb +12 -0
- data/lib/factory_bot/definition.rb +136 -0
- data/lib/factory_bot/definition_hierarchy.rb +48 -0
- data/lib/factory_bot/definition_proxy.rb +174 -0
- data/lib/factory_bot/errors.rb +25 -0
- data/lib/factory_bot/evaluation.rb +23 -0
- data/lib/factory_bot/evaluator.rb +82 -0
- data/lib/factory_bot/evaluator_class_definer.rb +20 -0
- data/lib/factory_bot/factory.rb +162 -0
- data/lib/factory_bot/factory_runner.rb +33 -0
- data/lib/factory_bot/find_definitions.rb +25 -0
- data/lib/factory_bot/linter.rb +97 -0
- data/lib/factory_bot/null_factory.rb +18 -0
- data/lib/factory_bot/null_object.rb +24 -0
- data/lib/factory_bot/registry.rb +38 -0
- data/lib/factory_bot/reload.rb +8 -0
- data/lib/factory_bot/sequence.rb +62 -0
- data/lib/factory_bot/strategy/attributes_for.rb +13 -0
- data/lib/factory_bot/strategy/build.rb +15 -0
- data/lib/factory_bot/strategy/create.rb +18 -0
- data/lib/factory_bot/strategy/null.rb +11 -0
- data/lib/factory_bot/strategy/stub.rb +108 -0
- data/lib/factory_bot/strategy_calculator.rb +26 -0
- data/lib/factory_bot/strategy_syntax_method_registrar.rb +54 -0
- data/lib/factory_bot/syntax.rb +7 -0
- data/lib/factory_bot/syntax/default.rb +76 -0
- data/lib/factory_bot/syntax/methods.rb +111 -0
- data/lib/factory_bot/syntax_runner.rb +6 -0
- data/lib/factory_bot/trait.rb +30 -0
- data/lib/factory_bot/version.rb +3 -0
- data/lib/factory_girl.rb +5 -0
- data/lib/factory_girl/version.rb +1 -0
- 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,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,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,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
|