diff --git a/lib/factory_bot/declaration/association.rb b/lib/factory_bot/declaration/association.rb index 123e73b9e..3413ea2a2 100644 --- a/lib/factory_bot/declaration/association.rb +++ b/lib/factory_bot/declaration/association.rb @@ -2,10 +2,10 @@ module FactoryBot class Declaration # @api private class Association < Declaration - def initialize(name, *options) + def initialize(name, *options, **overrides) super(name, false) @options = options.dup - @overrides = options.extract_options! + @overrides = overrides.dup @factory_name = @overrides.delete(:factory) || name @traits = options end @@ -13,16 +13,17 @@ def initialize(name, *options) def ==(other) self.class == other.class && name == other.name && - options == other.options + options == other.options && + overrides == other.overrides end protected - attr_reader :options + attr_reader :options, :overrides private - attr_reader :factory_name, :overrides, :traits + attr_reader :factory_name, :traits def build raise_if_arguments_are_declarations! diff --git a/lib/factory_bot/definition_proxy.rb b/lib/factory_bot/definition_proxy.rb index fc3d5dfc8..ace4f2ff0 100644 --- a/lib/factory_bot/definition_proxy.rb +++ b/lib/factory_bot/definition_proxy.rb @@ -88,17 +88,15 @@ def transient(&block) # end # # are equivalent. - def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing - association_options = args.first - - if association_options.nil? + def method_missing(name, *args, **kwargs, &block) # rubocop:disable Style/MissingRespondToMissing + if args.empty? && kwargs.empty? __declare_attribute__(name, block) - elsif __valid_association_options?(association_options) - association(name, association_options) + elsif __valid_association_options?(args, kwargs) + association(name, *args, **kwargs) else raise NoMethodError.new(<<~MSG) undefined method '#{name}' in '#{@definition.name}' factory - Did you mean? '#{name} { #{association_options.inspect} }' + Did you mean? '#{name} { #{args.first.inspect} }' MSG end end @@ -152,14 +150,14 @@ def sequence(name, *args, &block) # If no name is given, the name of the attribute is assumed to be the # name of the factory. For example, a "user" association will by # default use the "user" factory. - def association(name, *options) + def association(name, *options, **overrides) if block_given? raise AssociationDefinitionError.new( "Unexpected block passed to '#{name}' association " \ "in '#{@definition.name}' factory" ) else - declaration = Declaration::Association.new(name, *options) + declaration = Declaration::Association.new(name, *options, **overrides) @definition.declare_attribute(declaration) end end @@ -253,8 +251,8 @@ def __declare_attribute__(name, block) end end - def __valid_association_options?(options) - options.respond_to?(:has_key?) && options.has_key?(:factory) + def __valid_association_options?(options, overrides) + (!options.empty? && options.all?(Symbol)) || !overrides.empty? end ## diff --git a/spec/acceptance/traits_spec.rb b/spec/acceptance/traits_spec.rb index 710ccc647..00d285664 100644 --- a/spec/acceptance/traits_spec.rb +++ b/spec/acceptance/traits_spec.rb @@ -874,6 +874,55 @@ def initialize(name) end end +describe "traits used in implicit associations" do + before do + define_model("User", admin: :boolean, name: :string) + + define_model("Comment", text: :string, user_id: :integer) do + belongs_to :user + end + + define_model("Post", user_id: :integer) do + belongs_to :user, class_name: "User" + end + + FactoryBot.define do + factory :user do + admin { false } + + trait :admin do + admin { true } + end + + trait :robot do + name { "robot" } + end + end + + factory :comment do + text { "testing" } + user :admin, name: "Joe Slick" + end + + factory :post do + user :admin, :robot + end + end + end + + it "allows for a single inline trait with the default association" do + user = FactoryBot.create(:comment).user + expect(user).to be_admin + expect(user.name).to eq "Joe Slick" + end + + it "allows for multiple inline traits with the default association" do + user = FactoryBot.create(:post).user + expect(user).to be_admin + expect(user.name).to eq "robot" + end +end + describe "traits used in associations" do before do define_model("User", admin: :boolean, name: :string) diff --git a/spec/support/matchers/declaration.rb b/spec/support/matchers/declaration.rb index 65e4663bd..26642ccc6 100644 --- a/spec/support/matchers/declaration.rb +++ b/spec/support/matchers/declaration.rb @@ -40,8 +40,9 @@ def with_factory(factory) self end - def with_options(options) + def with_options(*options, **kwargs) @options = options + @kwargs = kwargs self end @@ -60,7 +61,7 @@ def expected_declaration when :implicit then FactoryBot::Declaration::Implicit.new(@name, @factory, ignored?) when :association if @options - FactoryBot::Declaration::Association.new(@name, options) + FactoryBot::Declaration::Association.new(@name, *options, **kwargs) else FactoryBot::Declaration::Association.new(@name) end @@ -72,7 +73,11 @@ def ignored? end def options - @options || {} + @options || [] + end + + def kwargs + @kwargs || {} end end end