لماذا الاختبار في Go ليس مخيفًا، بل ضروري
\n\nعندما تكتب بلغة Go لأول مرة، يبدو أن المترجم يلتقط كل شيء تقريبًا: الأنواع صارمة، الأخطاء واضحة، nil تحت السيطرة. ولكن بمجرد أن ينمو مشروعك إلى أكثر من 500 سطر، ستدرك: لا يمكنك العيش بدون اختبارات. تعديل واحد في مكان يكسر شيئًا آخر، وتكتشف ذلك فقط في بيئة الإنتاج.
في هذه المقالة، سأوضح لك من أين تبدأ اختبار Go كمطور مبتدئ. سنتعرف على اختبارات الوحدة، الاختبارات الجدولية، الكائنات الوهمية (Mocks) وتغطية الكود. لا ماء - فقط أمثلة عملية ونصائح عملية.
\n\nإذا كنت تكتب بلغة Go لمدة شهرين على الأقل، ستوفر لك هذه المادة ساعات من التصحيح.
\n\nما تحتاج معرفته قبل البدء
\n\nتوفر Go حزمة مدمجة testing - لا حاجة لتثبيت أي شيء إضافي. تُكتب الاختبارات في ملفات باللاحقة _test.go وتُشغل بالأمر:
go test ./...\n\nالقواعد الأساسية:
\n- \n
- يجب أن تسمى دالة الاختبار
TestXxx(t *testing.T)، حيث Xxx هو اسم الدالة المختبرة بحرف كبير. \n - ملفات الاختبارات تكون بجانب الكود المختبر. \n
- يتم الإبلاغ عن الأخطاء عبر
t.Error()أوt.Fatal(). \n
دعنا ننتقل فورًا إلى التطبيق العملي.
\n\nكتابة أول اختبار وحدة في Go
\n\nلنفترض أن لدينا دالة بسيطة تجمع رقمين:
\n\n// calc.go\npackage calc\n\nfunc Sum(a, b int) int {\n return a + b\n}\n\nننشئ ملف calc_test.go في نفس المجلد:
// calc_test.go\npackage calc\n\nimport "testing"\n\nfunc TestSum(t *testing.T) {\n result := Sum(2, 3)\n expected := 5\n\n if result != expected {\n t.Errorf("Sum(2,3) = %d; want %d", result, expected)\n }\n}\n\nنشغل الاختبار:
\n\ngo test -v\n\nسترى PASS باللون الأخضر - أول انتصار اختباري لك. لاحظ: نستخدم t.Errorf (غير قاتل) - سيستمر الاختبار حتى لو فشلت حالة واحدة.
الاختبارات الجدولية: عندما يكون اختبار واحد غير مريح
\n\nتخيل أنك بحاجة لاختبار دالة بعشرات من المدخلات المختلفة. هل تكتب 10 اختبارات منفصلة؟ لا، في Go لهذا الغرض يوجد الاختبارات الجدولية (table-driven tests). هذا هو المعيار في مجتمع Go.
\n\nنضيف إلى ملف calc.go دالة القسمة:
func Divide(a, b int) (int, error) {\n if b == 0 {\n return 0, errors.New("division by zero")\n }\n return a / b, nil\n}\n\nالآن نكتب اختبارًا جدوليًا:
\n\nfunc TestDivide(t *testing.T) {\n tests := []struct {\n name string\n a, b int\n expected int\n wantErr bool\n }{\n {"positive", 10, 2, 5, false},\n {"negative", -6, 3, -2, false},\n {"zero division", 5, 0, 0, true},\n {"same numbers", 7, 7, 1, false},\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n got, err := Divide(tt.a, tt.b)\n if tt.wantErr {\n if err == nil {\n t.Error("expected error, got nil")\n }\n return\n }\n if err != nil {\n t.Errorf("unexpected error: %v", err)\n }\n if got != tt.expected {\n t.Errorf("Divide(%d,%d) = %d; want %d", tt.a, tt.b, got, tt.expected)\n }\n })\n }\n}\n\nما الرائع هنا:
\n- \n
- كل حالة هي اختبار فرعي باسم. إذا فشلت واحدة، سيتم تنفيذ البقية. \n
- سهولة إضافة حالات جديدة - فقط أضف سطرًا إلى الهيكل. \n
- الكود يُقرأ كمواصفات. \n
شغّل مع العلم -v - سترى أسماء جميع الاختبارات الفرعية. هذا مريح جدًا أثناء التصحيح.