Использование factory_girl в Rails с ассоциациями, которые имеют уникальные ограничения. Получение дубликатов ошибок - PullRequest
40 голосов
/ 06 января 2010

Я работаю с проектом Rails 2.2 и работаю над его обновлением. Я заменяю существующие приборы фабриками (используя factory_girl) и у меня возникли некоторые проблемы. Проблема в моделях, которые представляют таблицы с поисковыми данными. Когда я создаю корзину с двумя продуктами с одинаковым типом продукта, каждый созданный продукт воссоздает один и тот же тип продукта. Это ошибки из-за уникальной проверки модели ProductType.

Проблема демонстрации

Это из модульного теста, где я создаю корзину и собираю ее по частям. Я должен был сделать это, чтобы обойти проблему. Это все еще демонстрирует проблему, хотя. Я объясню.

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 с типами ассоциаций "список выбора"?

Я бы хотел бы , чтобы фабричное определение содержало все, вместо того чтобы собирать его в тесте, хотя я могу жить с этим.

Фон и дополнительные детали

заводы / 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 состоит в том, чтобы приложение могло придать им особое значение. Модель 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

Ответы [ 10 ]

44 голосов
/ 10 июля 2012

Только к вашему сведению, вы также можете использовать макрос initialize_with внутри своей фабрики и проверить, существует ли объект, а не создавать его заново. Решение, использующее лямбду (это круто, но!), Это копирование логики, уже присутствующей в find_or_create_by. Это также работает для ассоциаций, в которых: league создается через связанную фабрику.

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
31 голосов
/ 25 августа 2010

Я столкнулся с той же проблемой и добавил лямбду в начало моего файла фабрики, который реализует шаблон синглтона, который также восстанавливает модель, если БД была очищена с момента последнего раунда тестов / спецификаций:

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 для запуска лямбды

Factory.define :product do |p|
  p.product_type  { single_instances[:add_product_type] }
  #...this block edited as per comment below
end

Voila!

2 голосов
/ 04 марта 2012

У меня была похожая ситуация. В итоге я использовал мой seed.rb для определения синглетонов и затем потребовал seed.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
2 голосов
/ 01 декабря 2011

EDIT:
См. Еще более чистое решение в конце этого ответа.

ОРИГИНАЛЬНЫЙ ОТВЕТ:
Это мое решение для создания синглтон-ассоциаций 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', т.е. синглтон.

Если вы хотите сохранить запрос в БД, вы можете вместо этого сделать:

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

Если вы хотите настроить более одной платформы и иметь разные наборы 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, поэтому первый вызов создает платформу, следующие два вызова находят уже созданную платформу.

Таким образом, все 3 версии платформы будут иметь одну и ту же платформу «NewFoo», и вы сможете создать столько наборов версий платформы, сколько вам нужно.

Я думаю, что это очень чистое решение, так как вы поддерживаете фабрику в чистоте и даже показываете читателю ваших тестовых шагов, что все эти 3 платформы имеют одинаковую платформу.

2 голосов
/ 19 января 2010

Эти проблемы будут устранены, когда синглтоны будут внедрены на фабриках - в настоящее время их значение составляет - http://github.com/roderickvd/factory_girl/tree/singletons Выпуск - http://github.com/thoughtbot/factory_girl/issues#issue/16

2 голосов
/ 13 января 2010

Короткий ответ: «Нет», у Фабричной девушки нет более чистого способа сделать это. Кажется, я проверял это на женских форумах Фабрики.

Однако я нашел другой ответ для себя. Это требует другого рода обходного пути, но делает все намного чище.

Идея состоит в том, чтобы изменить модели, которые представляют таблицы поиска, чтобы создать требуемую запись, если она отсутствует. Это нормально, потому что код ожидает существования определенных записей. Вот пример модифицированной модели.

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

Это именно то, что я хотел. Теперь я могу точно выразить свой заводской и тестовый код.

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

1 голос
/ 07 января 2010

У меня была такая же проблема, и я думаю, что это та же самая проблема, на которую здесь ссылаются: http://groups.google.com/group/factory_girl/browse_frm/thread/68947290d1819952/ef22581f4cd05aa9?tvc=1&q=associations+validates_uniqueness_of#ef22581f4cd05aa9

Я думаю, что ваш обходной путь, возможно, является лучшим решением проблемы.

0 голосов
/ 13 июня 2016

Вдохновленный ответами здесь, я нашел предложение @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

Пример использования его для создания четырех платформ_версии с одинаковой ссылкой на платформу:

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

0 голосов
/ 14 января 2010

Может быть, вы могли бы попытаться использовать последовательности factory_girl для названия типа продукта и полей кода? Я думаю, что для большинства тестов вас не волнует, является ли тип продукта кодом «code 1» или «sub», а для тех, которые вам нужны, вы всегда можете указать это явно.

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 
0 голосов
/ 07 января 2010

Я думаю, что, по крайней мере, нашел более чистый способ.

Мне нравится идея связаться с ThoughtBot для получения рекомендованного "официального" решения. Пока это работает хорошо.

Я только что объединил подход, заключающийся в том, чтобы делать это в коде теста, и делать все это в заводском определении.

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

Я обновлю, если найду лучшее решение.

...