لا تتجاوزوا أي callback في اختباراتكم
قصة callback واحد فاتني في Rails، وكلّفني يومًا كاملًا حتى عثرت عليه.

في كود SlimWiki، استغرق مني اختبار واحد فاشل يومًا كاملًا حتى عرفت سببه. والأسوأ أن الـ spec ينجح حين يُشغَّل المثال وحده، لكنه يفشل حين يُشغَّل ضمن مجموعة الاختبارات كاملة. هذه قصة callback واحد فاتني في Rails، وكلّفني يومًا كاملًا حتى عثرت عليه.
rspec spec/features/some_spec.rb:123 # passes
rspec spec/features/some_spec.rb # fails, wtf!
عندما بحثت في الأمر، تبيّن أن factory girl لدينا يُنتج حسابًا غير مدفوع حين نطلب منه حسابًا مدفوعًا.
pry> FactoryGirl.create(:paid_account).subscription.paid?
=> false
The :paid_account factory declares a :pro_subscription like this:
factory :paid_account, :parent => :account do association :subscription, :factory => :pro_subscription # ... end
But that factory works fine:
pry> FactoryGirl.create(:pro_subscription).paid? => true
After one day and sprinkling a bit of binding.pry here and there…
factory :paid_account, :parent => :account do
after(:build) do |account| binding.pry end end
It turned out that after building the :paid_account (but before saving), the subscription is paid, which is correct.
pry> account.subscription.paid? => true
After the account is saved, however, it turned into a non-paid account!
pry> account.save! => true pry> account.subscription.paid? => false # wtf!!
لا بد أن شيئًا ما يغيّر الحالة العامة (global state) للنظام، فما هو يا ترى؟ أحد الأسباب المحتملة هو تعديل الأصناف (classes) مباشرة لتجاوز callbacks مؤقتًا. لذلك بحثت بـ grep عن skip_callback في ملفات الـ specs، فوجدت هذا الـ factory الذي يبدو للوهلة الأولى بلا علاقة بالمشكلة:
factory :account_without_subscription, :parent => :account do
to_create do |account|
account.class.skip_callback :create, :after, :create_subscription
account.save!
account.class.set_callback :create, :after, :create_subscription
end
end
نحتاج أحيانًا إلى إنشاء حساب بلا اشتراك. ولتحقيق ذلك نتجاوز الـ callback مؤقتًا قبل إنشاء الحساب، ثم نعيده إلى الصنف بعد إنشاء الحساب.
يبدو الأمر سليمًا، أليس كذلك؟ انظروا الآن إلى ملف Account.rb لدينا:
after_create :create_subscription, :if => 'subscription.blank?'
لاحظوا شرط :if هنا، وغيابه في الـ factory أعلاه. هذا الفرق يجعل الاشتراك يُستبدل باشتراك غير مدفوع دون أي شرط. ويمتد أثر ذلك طوال تشغيل rspec، فيفشل كل اختبار لاحق يعتمد على اشتراك مدفوع.
تعلّمت اليوم أن تجاوز callbacks، أو تعديل سلوك أي صنف أثناء التشغيل عمومًا، أمر خطير ومعرّض للأخطاء.
البديل هو تعريف accessor:
# Attributes (for tests)
attr_accessor :_skip_creating_subscription
# ...
after_create :create_subscription, :if => 'subscription.blank?',
:unless => :_skip_creating_subscription
الشرطة السفلية في بداية اسم الخاصية تعني أنها مخصصة للاختبارات، وليست للاستخدام العام. وأصبح الـ factory لدينا أبسط بكثير أيضًا:
factory :account_without_subscription, :parent => :account do
_skip_creating_subscription { true }
end
أهمية callbacks في Rails
الـ callbacks في Rails أدوات قوية تتيح لكم تشغيل methods محددة في مراحل معيّنة من دورة حياة الكائن. وهي ضرورية للحفاظ على سلامة البيانات وأتمتة المهام، مثل إرسال بريد إلكتروني بعد تسجيل المستخدم، أو إنشاء سجلات مرتبطة بعد حفظ السجل الرئيسي. وفي سياق FactoryBot، قد يكون فهم الاستخدام الصحيح لـ callbacks هو الفرق بين مجموعة اختبارات متينة وأخرى كثيرة الأخطاء.
مخاطر تجاوز callbacks
من المغري تجاوز callbacks لتسهيل الاختبار، لكن لذلك عواقب غير مقصودة. فتجاوزها قد يُحدث تناقضات في بيئة الاختبار، ويؤدي إلى نتائج إيجابية أو سلبية كاذبة. فإذا كان الـ callback يضبط قيمة افتراضية أو يفرض قاعدة عمل، فإن تجاوزه ينتج بيانات في حالة غير صالحة لا تعكس بيئة الإنتاج.
بدائل تجاوز callbacks
بدلًا من تجاوز callbacks، فكّروا في استخدام منطق شرطي يتحكم في تنفيذها. أي أن تضيفوا شروطًا إلى الـ callbacks بحيث لا تُتجاوز إلا عند الحاجة. فاستخدام خاصية مثل _skip_creating_subscription مثلًا طريقة أنظف وأكثر أمانًا لإدارة تنفيذ callbacks دون المساس بالحالة العامة للتطبيق.
FactoryBot لاختبارات أكثر موثوقية
FactoryBot أداة ممتازة لتجهيز بيانات الاختبار، لكن جودتها مرهونة بجودة إعدادها. احرصوا على أن تكون الـ factories معرّفة جيدًا، وأن تُستخدم callbacks بحكمة. وهذا يعني التفكير جيدًا في متى تستخدمون after_create ومتى تستخدمون after_build، والتأكد من أن أي منطق شرطي موثّق ومفهوم لدى فريقكم.
callbacks في الاختبارات
لتحافظوا على مجموعة اختبارات سليمة، اتبعوا ما يلي:
- وثّقوا الغرض من كل callback وأثره في النظام.
- استخدموا الشروط للتحكم في callbacks بدلًا من تجاوزها.
- راجعوا الـ factories وأعيدوا هيكلتها بانتظام لتتوافق مع قواعد العمل.
- لا تعدّلوا الحالة العامة في الاختبارات، تجنبًا لآثار جانبية على الاختبارات الأخرى.
باتباع هذه النصائح ستتمكنون من استخدام callbacks دون الإضرار بمجموعة اختباراتكم.
والآن عدنا إلى تشغيلنا الرائع!


