استخدام Factory_girl في Rails مع الارتباطات التي لها قيود فريدة.الحصول على أخطاء مكررة

StackOverflow https://stackoverflow.com/questions/2015473

سؤال

أنا أعمل مع مشروع 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 مع أنواع الارتباطات "قائمة الاختيار"؟

بطاقة تعريف يحب لكي يحتوي تعريف المصنع على كل شيء بدلاً من الاضطرار إلى تجميعه في الاختبار، على الرغم من أنني أستطيع التعايش معه.

الخلفية والتفاصيل الإضافية

المصانع/المنتج.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

المصانع/عربة.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
هل كانت مفيدة؟

المحلول

لقد واجهت نفس المشكلة وأضفت lambda في الجزء العلوي من ملف المصانع الخاص بي والذي ينفذ نمطًا مفردًا، والذي يقوم أيضًا بإعادة إنشاء النموذج إذا تم مسح قاعدة البيانات منذ الجولة الأخيرة من الاختبارات/المواصفات:

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 لتشغيل lambda

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

هاهو!

نصائح أخرى

لمعلوماتك فقط، يمكنك أيضًا استخدام 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

الإجابة المختصرة هي "لا"، ليس لدى فتاة المصنع طريقة أفضل للقيام بذلك.يبدو أنني قمت بالتحقق من ذلك في منتديات فتيات المصنع.

ومع ذلك، وجدت إجابة أخرى لنفسي.إنه يتضمن نوعًا آخر من الحلول ولكنه يجعل كل شيء أكثر نظافة.

تتمثل الفكرة في تغيير النماذج التي تمثل جداول البحث لإنشاء الإدخال المطلوب في حالة فقدانه.وهذا أمر جيد لأن الكود يتوقع وجود إدخالات محددة.هنا مثال على النموذج المعدل.

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.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.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

هذا هو بالضبط ما أردت.يمكنني الآن التعبير بوضوح عن رمز المصنع والاختبار الخاص بي.

فائدة أخرى لهذا الأسلوب هي أن بيانات جدول البحث لا تحتاج إلى أن تكون مصنفة أو مملوءة في عمليات الترحيل.سوف يتعامل مع قواعد بيانات الاختبار وكذلك الإنتاج.

سيتم القضاء على هذه المشاكل عندما يتم إدخال المفردات إلى المصانع - وهي في الوقت الحالي -http://github.com/roderickvd/factory_girl/tree/singletonsمشكلة - http://github.com/thinkbot/factory_girl/issues#issue/16

يحرر:
شاهد حلاً أنظف في أسفل هذه الإجابة.

الإجابة الأصلية:
هذا هو الحل الخاص بي لإنشاء جمعيات فردية لـ 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  |

بهذه الطريقة، سيكون لجميع إصدارات الأنظمة الأساسية الثلاثة نفس النظام الأساسي "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

إذا كنت ترغب في إعداد أكثر من منصة واحدة، ولديك مجموعات مختلفة من إصدارات_المنصة، فيمكنك إنشاء سمات مختلفة أكثر تحديدًا، على سبيل المثال:

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

آمل أن يكون هذا مفيدًا للبعض هناك.

يحرر:
بعد تقديم الحل الأصلي أعلاه، قمت بإلقاء نظرة أخرى على الكود، ووجدت طريقة أكثر وضوحًا للقيام بذلك:لا يمكنك تحديد السمات في المصانع، وبدلاً من ذلك يمكنك تحديد الارتباط عند استدعاء خطوة الاختبار.

عمل مصانع عادية:

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"، لذا فإن الاستدعاء الأول ينشئ النظام الأساسي، بينما تعثر الاستدعاءتان التاليتان على النظام الأساسي الذي تم إنشاؤه بالفعل.

بهذه الطريقة، سيكون لجميع إصدارات الأنظمة الأساسية الثلاثة نفس النظام الأساسي "NewFoo"، ويمكنك إنشاء أي عدد تريده من مجموعات إصدارات النظام الأساسي.

أعتقد أن هذا حل نظيف للغاية، نظرًا لأنك تحافظ على نظافة المصنع، بل وتوضح لقارئ خطوات الاختبار الخاصة بك أن إصدارات الأنظمة الأساسية الثلاثة جميعها لها نفس النظام الأساسي.

كان لدى موقف مشابه.انتهى بي الأمر باستخدام 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

لقد واجهت نفس المشكلة وأعتقد أنها نفس المشكلة المشار إليها هنا: http://groups.google.com/group/factory_girl/browse_frm/thread/68947290d1819952/ef22581f4cd05aa9?tvc=1&q=associations+validates_uniqueness_of#ef22581f4cd05aa9

أعتقد أن الحل البديل الخاص بك ربما يكون الحل الأفضل للمشكلة.

ربما يمكنك تجربة استخدام تسلسلات 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 

أعتقد أنني على الأقل وجدت طريقة أنظف.

تعجبني فكرة الاتصال بـ 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

سوف أقوم بالتحديث إذا وجدت حلاً أفضل.

مستوحاة من الإجابات هنا، وجدت أن اقتراح @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

يبدو الأمر وكأنه الأفضل في كلا العالمين دون ثني فتاة المصنع بعيدًا عن الشكل.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top