Подтвердить что ты не робот

Использование 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
4b9b3361

Ответ 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 и тестовый код.

Другим преимуществом такого подхода является то, что данные таблицы поиска не должны быть посещены или заполнены миграциями. Он будет обрабатывать себя как для тестовых баз данных, так и для производства.

Ответ 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

Ответ 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 слишком далеко от формы.