إعادة بناء بثقة عبر الاختبارات
Refactor with Tests
إعادة بناء بثقة عبر الاختبارات — Refactor with Tests
الاختبار ليس فقط لاكتشاف الأخطاء بعد وقوعها. هو شبكة أمان عندما تغيّر شكل الكود وتريد الحفاظ على السلوك.
إعادة البناء تعني تغيير شكل الكود دون تغيير السلوك الخارجي. هذا تعريف بسيط لكنه يحتاج انضباطاً. إذا بدأت بتعديل الدالة قبل تثبيت السلوك، فلن تعرف هل حسّنت القراءة فقط أم كسرت قاعدة عمل مهمة. الاختبارات تعطيك خطاً واضحاً: هذه الحالات كانت تعمل، ويجب أن تظل تعمل بعد التغيير.
في هذا الدرس سنستخدم مثال رسوم توصيل. القواعد صغيرة، لكنها تكفي لتوضيح الفكرة: مدينة معينة لها حد شحن مجاني، وبقية المدن لها قاعدة مختلفة. عندما ترى شروطاً متداخلة، لا تبدأ مباشرة بنقل الأسطر. اكتب الحالات المهمة أولاً، ثم غيّر بثقة.
الحالة: حساب رسوم توصيل
لدينا دالة تعمل، لكنها مليئة بالشروط المتداخلة.
func deliveryFee(city string, total float64) float64 {
if city == "الرياض" {
if total >= 200 {
return 0
}
return 15
}
if total >= 300 {
return 10
}
return 25
}
قبل إعادة البناء، نثبت السلوك بجدول اختبارات.
لاحظ أن “الكود قبيح” ليس سبباً كافياً لتغييره بلا حماية. السبب العملي هو أن الشروط المتداخلة تجعل إضافة مدينة أو قاعدة جديدة أصعب. لكن قبل أن نرتبها، نحتاج معرفة السلوك الحالي بالضبط: الرياض تحت 200، الرياض فوق 200، مدينة أخرى تحت 300، ومدينة أخرى فوق 300.
اختبار السلوك الحالي
هذا المثال يطبع النتائج بدلاً من استخدام go test لأننا داخل درس تفاعلي. في مشروع حقيقي ستكون نفس الحالات داخل table-driven test. القيمة هنا في الجدول نفسه: كل حالة تحمل المدخلات والنتيجة المتوقعة، وهذا يجعل إضافة حالة جديدة أسهل من كتابة اختبار منفصل لكل فرع.
إعادة بناء أوضح
نستخرج قواعد الشحن إلى دوال صغيرة. السلوك نفسه، القراءة أسهل.
استخراج isLocal ليس مطلوباً دائماً، لكنه يوضح نية الشرط. إذا تغير تعريف المحلي لاحقاً ليشمل الرياض والدرعية مثلاً، فستعدل مكاناً واحداً. ومع ذلك، لا تستخرج دوالاً صغيرة بلا معنى. الدالة الجيدة تحمل مفهوماً في المجال، لا مجرد سطر نقلته لإخفاء التعقيد.
تحدي موجه
أكمل دالة runTests بحيث تطبع نجاح كل حالة. الفكرة هنا أن تتدرب على عقلية table-driven tests حتى داخل playground.
عند كتابة ملف اختبار حقيقي
في مشروع Go حقيقي، ستضع الحالات السابقة داخل ملف مثل delivery_test.go وتستخدم:
if got != tc.want {
t.Fatalf("deliveryFee(%q, %.0f) = %.0f, want %.0f", tc.city, tc.total, got, tc.want)
}
خطوات إعادة بناء آمنة
ابدأ بتسمية السلوك الحالي في حالات. لا تكتف بحالة واحدة ناجحة؛ اختر الحدود التي قد تكسرها الشروط، مثل 200 و300 في هذا المثال. بعد ذلك شغّل الاختبارات وتأكد أنها تفشل إذا غيّرت نتيجة متوقعة عمداً، لأن الاختبار الذي لا يستطيع الفشل لا يحميك. ثم عدّل الكود على دفعات صغيرة، وشغّل الاختبارات بعد كل دفعة.
من الأخطاء الشائعة أن تغيّر السلوك وتسمّيه refactor. إذا قررت أن الشحن في جدة فوق 300 يجب أن يصبح مجانياً، فهذا تغيير ميزة أو قاعدة عمل، لا إعادة بناء. افصل النوعين في ذهنك وفي commits: اختبار السلوك الحالي، إعادة بناء تحفظه، ثم تغيير قاعدة العمل باختبار جديد إذا كان مطلوباً.
خلاصة
الاختبارات تعطيك شجاعة تقنية لكنها لا تعفيك من التفكير. جدول الحالات يصف العقد، والدالة الجديدة يجب أن تحترمه. كلما صار الكود أوضح مع بقاء الاختبارات خضراء، زادت ثقتك أن التحسين حقيقي وليس مجرد إعادة ترتيب خطرة. هذه العادة مهمة في خدمات الإنتاج، لأن أكثر الأخطاء إزعاجاً تحدث عندما نكسر سلوكاً قديماً أثناء تحسين كود يبدو بسيطاً.