استخدام Factory_girl في Rails مع الارتباطات التي لها قيود فريدة.الحصول على أخطاء مكررة
-
19-09-2019 - |
سؤال
أنا أعمل مع مشروع 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
يبدو الأمر وكأنه الأفضل في كلا العالمين دون ثني فتاة المصنع بعيدًا عن الشكل.