Использование factory_girl в Rails с ассоциациями, которые имеют уникальные ограничения. Получение повторяющихся ошибок
Я работаю над проектом Rails 2.2, который обновляет его. Я заменяю существующие светильники фабриками (с использованием factory_girl), и у вас были некоторые проблемы. Проблема заключается в моделях, которые представляют таблицы с данными поиска. Когда я создаю корзину с двумя продуктами, имеющими один и тот же тип продукта, каждый созданный продукт воссоздает тот же тип продукта. Это ошибки от уникальной проверки на модели ProductType.
Демонстрация проблем
Это из unit test, где я создаю корзину и складываю ее в куски. Я должен был сделать это, чтобы решить проблему. Однако это все еще демонстрирует проблему. Я объясню.
cart = Factory(:cart)
cart.cart_items = [Factory(:cart_item,
:cart => cart,
:product => Factory(:added_users_product)),
Factory(:cart_item,
:cart => cart,
:product => Factory(:added_profiles_product))]
Два добавляемых продукта одного типа, и когда каждый продукт создается, он воссоздает тип продукта и создает дубликаты.
Ошибка генерируется:
"ActiveRecord:: RecordInvalid: сбой проверки: имя уже выполнено, код уже был принят"
Обход
Обходной путь для этого примера - переопределить используемый тип продукта и передать его в конкретном экземпляре, поэтому используется только один экземпляр. "Add_product_type" выбирается раньше и передается для каждого элемента корзины.
cart = Factory(:cart)
prod_type = Factory(:add_product_type) #New
cart.cart_items = [Factory(:cart_item,
:cart => cart,
:product => Factory(:added_users_product,
:product_type => prod_type)), #New
Factory(:cart_item,
:cart => cart,
:product => Factory(:added_profiles_product,
:product_type => prod_type))] #New
Вопрос
Каков наилучший способ использования factory_girl с типами "pick-list"?
Я бы хотел, чтобы определение factory содержало все, а не собирать его в тесте, хотя я могу жить с ним.
Фон и дополнительные данные
заводы/product.rb
# Declare ProductTypes
Factory.define :product_type do |t|
t.name "None"
t.code "none"
end
Factory.define :sub_product_type, :parent => :product_type do |t|
t.name "Subscription"
t.code "sub"
end
Factory.define :add_product_type, :parent => :product_type do |t|
t.name "Additions"
t.code "add"
end
# Declare Products
Factory.define :product do |p|
p.association :product_type, :factory => :add_product_type
#...
end
Factory.define :added_profiles_product, :parent => :product do |p|
p.association :product_type, :factory => :add_product_type
#...
end
Factory.define :added_users_product, :parent => :product do |p|
p.association :product_type, :factory => :add_product_type
#...
end
Цель ProductType "code" заключается в том, что приложение может придать им особый смысл. Модель ProductType выглядит примерно так:
class ProductType < ActiveRecord::Base
has_many :products
validates_presence_of :name, :code
validates_uniqueness_of :name, :code
#...
end
заводы/cart.rb
# Define Cart Items
Factory.define :cart_item do |i|
i.association :cart
i.association :product, :factory => :test_product
i.quantity 1
end
Factory.define :cart_item_sub, :parent => :cart_item do |i|
i.association :product, :factory => :year_sub_product
end
Factory.define :cart_item_add_profiles, :parent => :cart_item do |i|
i.association :product, :factory => :add_profiles_product
end
# Define Carts
# Define a basic cart class. No cart_items as it creates dups with lookup types.
Factory.define :cart do |c|
c.association :account, :factory => :trial_account
end
Factory.define :cart_with_two_different_items, :parent => :cart do |o|
o.after_build do |cart|
cart.cart_items = [Factory(:cart_item,
:cart => cart,
:product => Factory(:year_sub_product)),
Factory(:cart_item,
:cart => cart,
:product => Factory(:added_profiles_product))]
end
end
Когда я пытаюсь определить корзину с двумя элементами одного и того же типа продукта, я получаю ту же ошибку, описанную выше.
Factory.define :cart_with_two_add_items, :parent => :cart do |o|
o.after_build do |cart|
cart.cart_items = [Factory(:cart_item,
:cart => cart,
:product => Factory(:added_users_product)),
Factory(:cart_item,
:cart => cart,
:product => Factory(:added_profiles_product))]
end
end
Ответы
Ответ 1
Я столкнулся с той же проблемой и добавил лямбда в верхней части моего файла фабрик, который реализует одноэлементный шаблон, который также регенерирует модель, если db был очищен с момента последнего раунда тестов/спецификаций:
saved_single_instances = {}
#Find or create the model instance
single_instances = lambda do |factory_key|
begin
saved_single_instances[factory_key].reload
rescue NoMethodError, ActiveRecord::RecordNotFound
#was never created (is nil) or was cleared from db
saved_single_instances[factory_key] = Factory.create(factory_key) #recreate
end
return saved_single_instances[factory_key]
end
Затем, используя ваши фабрики примеров, вы можете использовать атрибут factory_girl lazy для запуска lambda
Factory.define :product do |p|
p.product_type { single_instances[:add_product_type] }
#...this block edited as per comment below
end
Voila!
Ответ 2
Просто FYI, вы также можете использовать макрос initialize_with
внутри вашего factory и проверить, существует ли объект уже, а затем не создавать его снова. Решение, использующее лямбда (его удивительный, но!), Реплицирует логику, уже присутствующую в find_or_create_by. Это также работает для ассоциаций, где: лига создается через связанный factory.
FactoryGirl.define do
factory :league, :aliases => [:euro_cup] do
id 1
name "European Championship"
rank 30
initialize_with { League.find_or_create_by_id(id)}
end
end
Ответ 3
Короткий ответ: "нет", Factory девушка не имеет более чистого способа сделать это. Кажется, я проверил это на форумах Factory.
Однако я нашел для себя другой ответ. Это связано с другим видом обхода, но делает все намного более чистым.
Идея состоит в том, чтобы изменить модели, которые представляют таблицы поиска, чтобы создать требуемую запись, если она отсутствует. Это нормально, потому что код ожидает наличия определенных записей. Вот пример модифицированной модели.
class ProductType < ActiveRecord::Base
has_many :products
validates_presence_of :name, :code
validates_uniqueness_of :name, :code
# Constants defined for the class.
CODE_FOR_SUBSCRIPTION = "sub"
CODE_FOR_ADDITION = "add"
# Get the ID for of the entry that represents a trial account status.
def self.id_for_subscription
type = ProductType.find(:first, :conditions => ["code = ?", CODE_FOR_SUBSCRIPTION])
# if the type wasn't found, create it.
if type.nil?
type = ProductType.create!(:name => 'Subscription', :code => CODE_FOR_SUBSCRIPTION)
end
# Return the loaded or created ID
type.id
end
# Get the ID for of the entry that represents a trial account status.
def self.id_for_addition
type = ProductType.find(:first, :conditions => ["code = ?", CODE_FOR_ADDITION])
# if the type wasn't found, create it.
if type.nil?
type = ProductType.create!(:name => 'Additions', :code => CODE_FOR_ADDITION)
end
# Return the loaded or created ID
type.id
end
end
Статический метод класса "id_for_addition" будет загружать модель и идентификатор, если они найдены, если они не найдены, это создаст его.
Недостатком метода "id_for_addition" может быть неясно, что он делает по его имени. Возможно, это изменится. Единственным другим воздействием кода для нормального использования является дополнительный тест, чтобы узнать, найдена ли модель или нет.
Это означает, что код Factory для создания продукта можно изменить следующим образом:
Factory.define :added_users_product, :parent => :product do |p|
#p.association :product_type, :factory => :add_product_type
p.product_type_id { ProductType.id_for_addition }
end
Это означает, что модифицированный код Factory может выглядеть так:
Factory.define :cart_with_two_add_items, :parent => :cart do |o|
o.after_build do |cart|
cart.cart_items = [Factory(:cart_item_add_users, :cart => cart),
Factory(:cart_item_add_profiles, :cart => cart)]
end
end
Это именно то, что я хотел. Теперь я могу четко выразить свой Factory и тестовый код.
Другим преимуществом такого подхода является то, что данные таблицы поиска не должны быть посещены или заполнены миграциями. Он будет обрабатывать себя как для тестовых баз данных, так и для производства.
Ответ 4
Эти проблемы будут устранены, когда синглтоны будут введены на фабрики, а в настоящее время - http://github.com/roderickvd/factory_girl/tree/singletons
Проблема - http://github.com/thoughtbot/factory_girl/issues#issue/16
Ответ 5
EDIT:
См. Решение с четным фильтром в нижней части этого ответа.
ОРИГИНАЛЬНЫЙ ОТВЕТ:
Это мое решение для создания ассоциаций Singleton в FactoryGirl:
FactoryGirl.define do
factory :platform do
name 'Foo'
end
factory :platform_version do
name 'Bar'
platform {
if Platform.find(:first).blank?
FactoryGirl.create(:platform)
else
Platform.find(:first)
end
}
end
end
Вы называете это, например. как:
And the following platform versions exists:
| Name |
| Master |
| Slave |
| Replica |
Таким образом, все 3 версии платформы будут иметь одну и ту же платформу Foo, т.е. singleton.
Если вы хотите сохранить запрос db, вы можете:
platform {
search = Platform.find(:first)
if search.blank?
FactoryGirl.create(:platform)
else
search
end
}
И вы можете подумать о том, чтобы сделать синтаксическую ассоциацию признаком:
factory :platform_version do
name 'Bar'
platform
trait :singleton do
platform {
search = Platform.find(:first)
if search.blank?
FactoryGirl.create(:platform)
else
search
end
}
end
factory :singleton_platform_version, :traits => [:singleton]
end
Если вы хотите настроить более 1 платформу и иметь разные наборы platform_versions, вы можете создавать различные особенности, которые более специфичны, то есть:
factory :platform_version do
name 'Bar'
platform
trait :singleton do
platform {
search = Platform.find(:first)
if search.blank?
FactoryGirl.create(:platform)
else
search
end
}
end
trait :newfoo do
platform {
search = Platform.find_by_name('NewFoo')
if search.blank?
FactoryGirl.create(:platform, :name => 'NewFoo')
else
search
end
}
end
factory :singleton_platform_version, :traits => [:singleton]
factory :newfoo_platform_version, :traits => [:newfoo]
end
Надеюсь, что это полезно для некоторых.
EDIT:
После отправки моего первоначального решения выше, я еще раз взглянул на код и нашел еще более чистый способ сделать это: вы не определяете черты на фабриках, вместо этого вы указываете ассоциацию при вызове этапа тестирования.
Сделайте регулярные заводы:
FactoryGirl.define do
factory :platform do
name 'Foo'
end
factory :platform_version do
name 'Bar'
platform
end
end
Теперь вы вызываете тестовый шаг с указанной ассоциацией:
And the following platform versions exists:
| Name | Platform |
| Master | Name: NewFoo |
| Slave | Name: NewFoo |
| Replica | Name: NewFoo |
При выполнении этого, при создании платформы NewFoo используется функция "find_or_create_by", поэтому первый вызов создает платформу, а следующие 2 вызова находят уже созданную платформу.
Таким образом, все 3 версии платформы будут иметь одну и ту же платформу "NewFoo", и вы можете создать столько наборов версий платформы, сколько вам нужно.
Я думаю, что это очень чистое решение, так как вы сохраняете factory чистым, и вы даже делаете его видимым для читателя ваших тестовых шагов, что эти 3 версии платформы имеют одну и ту же платформу.
Ответ 6
У меня была аналогичная ситуация. Я закончил использование моего seeds.rb для определения синглтонов, а затем потребовал, чтобы seeds.rb в spec_helper.rb создавал объекты в тестовой базе данных. Затем я могу просто найти соответствующий объект на фабриках.
дб/seeds.rb
RegionType.find_or_create_by_region_type('community')
RegionType.find_or_create_by_region_type('province')
спецификации/spec_helper.rb
require "#{Rails.root}/db/seeds.rb"
спецификации/factory.rb
FactoryGirl.define do
factory :region_community, class: Region do
sequence(:name) { |n| "Community#{n}" }
region_type { RegionType.find_by_region_type("community") }
end
end
Ответ 7
У меня была такая же проблема, и я думаю, что она упоминается здесь: http://groups.google.com/group/factory_girl/browse_frm/thread/68947290d1819952/ef22581f4cd05aa9?tvc=1&q=associations+validates_uniqueness_of#ef22581f4cd05aa9
Я думаю, что ваше решение проблемы - лучшее решение проблемы.
Ответ 8
Я думаю, что я, по крайней мере, нашел более чистый способ.
Мне нравится идея связаться с ThoughtBot о получении рекомендуемого "официального" решения. Пока это хорошо работает.
Я только что объединил подход к этому в тестовом коде, выполнив все это в определении factory.
Factory.define :cart_with_two_add_items, :parent => :cart do |o|
o.after_build do |cart|
prod_type = Factory(:add_product_type) # Define locally here and reuse below
cart.cart_items = [Factory(:cart_item,
:cart => cart,
:product => Factory(:added_users_product,
:product_type => prod_type)),
Factory(:cart_item,
:cart => cart,
:product => Factory(:added_profiles_product,
:product_type => prod_type))]
end
end
def test_cart_with_same_item_types
cart = Factory(:cart_with_two_add_items)
# ... Do asserts
end
Я обновлю, если найду лучшее решение.
Ответ 9
Возможно, вы могли бы попробовать использовать последовательности factory_girl для имени типа продукта и полей кода? Для большинства тестов, я думаю, вам будет безразлично, будет ли код типа продукта "кодом 1" или "под", а для тех, кого вы заботитесь, вы всегда можете указать это явно.
Factory.sequence(:product_type_name) { |n| "ProductType#{n}" }
Factory.sequence(:product_type_code) { |n| "prod_#{n}" }
Factory.define :product_type do |t|
t.name { Factory.next(:product_type_name) }
t.code { Factory.next(:product_type_code) }
end
Ответ 10
Вдохновленный ответами здесь, я нашел предложение от @Jonas Bang ближе всего к моим потребностям. Вот что сработало для меня в середине 2016 года (FactoryGirl v4.7.0, Rails 5rc1):
FactoryGirl.define do
factory :platform do
name 'Foo'
end
factory :platform_version do
name 'Bar'
platform { Platform.first || create(:platform) }
end
end
Пример использования его для создания четырех platform_version с той же ссылкой на платформу:
FactoryGirl.create :platform_version
FactoryGirl.create :platform_version, name: 'Car'
FactoryGirl.create :platform_version, name: 'Dar'
=>
-------------------
platform_versions
-------------------
name | platform
------+------------
Bar | Foo
Car | Foo
Dar | Foo
И если вам нужна "Дар" на отдельной платформе:
FactoryGirl.create :platform_version
FactoryGirl.create :platform_version, name: 'Car'
FactoryGirl.create :platform_version, name: 'Dar', platform: create(:platform, name: 'Goo')
=>
-------------------
platform_versions
-------------------
name | platform
------+------------
Bar | Foo
Car | Foo
Dar | Goo
Чувствует себя лучшим из обоих миров, не изгибая factory_girl слишком далеко от формы.