FactoryGirl: почему атрибуты пропускают некоторые атрибуты?
Я хочу использовать FactoryGirl.attributes_for для тестирования контроллера, как в:
it "raise error creating a new PremiseGroup for this user" do
expect {
post :create, {:premise_group => FactoryGirl.attributes_for(:premise_group)}
}.to raise_error(CanCan::AccessDenied)
end
... но это не работает, потому что #attributes_for исключает атрибут: user_id. Вот разница между #create
и #attributes_for
:
>> FactoryGirl.create(:premise_group)
=> #<PremiseGroup id: 3, name: "PremiseGroup_4", user_id: 6, is_visible: false, is_open: false)
>> FactoryGirl.attributes_for(:premise_group)
=> {:name=>"PremiseGroup_5", :is_visible=>false, :is_open=>false}
Обратите внимание, что: user_id отсутствует в #attributes_for
. Это ожидаемое поведение?
FWIW, файл моих фабрик включает определения для :premise_group
и для :user
:
FactoryGirl.define do
...
factory :premise_group do
sequence(:name) {|n| "PremiseGroup_#{n}"}
user
is_visible false
is_open false
end
factory :user do
...
end
end
Ответы
Ответ 1
Копаем глубоко в документацию FactoryGirl, например. эта страница wiki, вы найдете упоминания о том, что attributes_for
игнорирует ассоциации. Наше дело не в том, чтобы задаться вопросом, почему (но я действительно представил вопрос) (но см. Обновление ниже). В качестве обходного пути я обернул вспомогательный метод вокруг FactoryGirl.build(...).attributes
, который разбивает id
, created_at
и updated_at
:
def build_attributes(*args)
FactoryGirl.build(*args).attributes.delete_if do |k, v|
["id", "created_at", "updated_at"].member?(k)
end
end
Итак, теперь:
>> build_attributes(:premise_group)
=> {"name"=>"PremiseGroup_21", "user_id"=>29, "is_visible"=>false, "is_open"=>false}
... это именно то, что ожидалось.
Обновление
Взяв комментарии от создателей FactoryGirl, я понимаю, почему attributes_for
игнорирует ассоциации: ссылка на ассоциацию генерирует вызов к db, который в некоторых случаях может значительно замедлить тесты. Но если вам нужны ассоциации, подход build_attributes
, показанный выше, должен работать.
Ответ 2
Я думаю, что это небольшое улучшение по сравнению с бесстрашным ответом, хотя это зависит от вашего желаемого результата.
Проще всего объяснить с помощью примера. Скажем, у вас есть лат и длинные атрибуты в вашей модели. На вашей форме у вас нет лат и длинных полей, но лат-градус, лат-мин, лат-секунда и т.д. Эти позже могут быть преобразованы в десятичную длинную длинную форму.
Скажите, что ваш factory выглядит так:
factory :something
lat_d 12
lat_m 32
..
long_d 23
long_m 23.2
end
Бесстрашный build_attributes
вернет { lat: nil, long: nil}
. Пока build_attributes
ниже вернет { lat_d: 12, lat_m: 32..., lat: nil...}
def build_attributes
ba = FactoryGirl.build(*args).attributes.delete_if do |k, v|
["id", "created_at", "updated_at"].member?(k)
end
af = FactoryGirl.attributes_for(*args)
ba.symbolize_keys.merge(af)
end
Ответ 3
Чтобы подробнее разобраться с данным решением build_attributes
, я изменил его, чтобы добавить только доступные ассоциации:
def build_attributes(*args)
obj = FactoryGirl.build(*args)
associations = obj.class.reflect_on_all_associations(:belongs_to).map { |a| "#{a.name}_id" }
accessible = obj.class.accessible_attributes
accessible_associations = obj.attributes.delete_if do |k, v|
!associations.member?(k) or !accessible.include?(k)
end
FactoryGirl.attributes_for(*args).merge(accessible_associations.symbolize_keys)
end
Ответ 4
Вот еще один способ:
FactoryGirl.build(:car).attributes.except('id', 'created_at', 'updated_at').symbolize_keys
Ограничения:
- Он не генерирует атрибуты для ассоциаций HMT и HABTM (поскольку эти ассоциации хранятся в таблице соединений, а не фактическом атрибуте).
- Стратегия ассоциации в factory должна быть
create
, как в association :user, strategy: :create
. Эта стратегия может сделать ваш factory очень медленным, если вы не используете его с умом.
Ответ 5
Принятый ответ кажется устаревшим, так как он не работал для меня, после того, как я покопался в Интернете и особенно в этой проблеме Github, я представляю вам:
Чистая версия для самых основных функциональных возможностей Rails 5+
Это создает ассоциации :belongs_to
и добавляет их id
(и type
if :polymorphic
) к атрибутам. Он также включает в себя код через FactoryBot::Syntax::Methods
вместо собственного модуля, ограниченного контроллерами.
Спецификация/поддержка /factory_bot_macros.rb
module FactoryBot::Syntax::Methods
def nested_attributes_for(*args)
attributes = attributes_for(*args)
klass = args.first.to_s.camelize.constantize
klass.reflect_on_all_associations(:belongs_to).each do |r|
association = FactoryBot.create(r.class_name.underscore)
attributes["#{r.name}_id"] = association.id
attributes["#{r.name}_type"] = association.class.name if r.options[:polymorphic]
end
attributes
end
end
это адаптированная версия jamesst20 по выпуску github - слава ему 👏