مقدمة: الاختبارات تنجح، لكن الأخطاء باقية؟
\n\nتخيل الموقف: لقد كتبت وحدة برمجية بلغة Go، وغطيتها باختبارات الوحدة بنسبة 95% وفقًا لـ go test -cover، وجميع الاختبارات خضراء. أنت واثق من أن الكود يعمل بشكل مثالي. ولكن في بيئة الإنتاج يظهر خطأ. هل هذا مألوف؟
المشكلة هي أن مقياس تغطية الكود (code coverage) هو مجرد عدد الأسطر التي تم تنفيذها. إنه لا يخبرك بما تختبره الاختبارات بالضبط. يمكن أن يكون لديك تغطية 100%، ولكنك لم تتحقق أبدًا من أن الدالة ترجع القيمة الصحيحة، وليس فقط أنها لا تتعطل مع خطأ panic.
\n\nهنا يأتي دور الاختبار الطفري (mutation testing) في Go - وهي منهجية تقوم بإدخال أخطاء بشكل مصطنع في الكود الخاص بك وترى ما إذا كانت الاختبارات قادرة على اكتشافها. إذا لم تفشل الاختبارات على الكود المُطفّر، فهذا يعني أن اختباراتك عديمة الفائدة.
\n\nفي هذه المقالة، سنشرح ما هو الاختبار الطفري في Go، وكيفية إعداده، ولماذا هو أمر لا غنى عنه للمشاريع التي تكون فيها الموثوقية أمرًا بالغ الأهمية. هيا بنا!
\n\nما هو الاختبار الطفري؟
\n\nالاختبار الطفري هو طريقة لتقييم جودة الاختبارات، حيث يتم إجراء تغييرات صغيرة (طفرات) على البرنامج تحاكي الأخطاء النموذجية للمطور. ثم يتم تشغيل الاختبارات. إذا فشل اختبار واحد على الأقل - يتم "قتل" الطفرة، والاختبار يعمل. إذا نجحت جميع الاختبارات - "نجت" الطفرة، وهذه علامة حمراء: لديك ثغرة في الاختبارات.
\n\nفي Go، يُشار إلى الاختبار الطفري غالبًا باسم mutation testing golang، وهو مفيد بشكل خاص للتحقق من منطق الأعمال، والمُحققّات (validators)، والمُحلّلات (parsers)، والمكونات الهامة الأخرى.
\n\nمثال على طفرة
\n\nلنفترض أن لديك دالة بسيطة:
\n\nfunc IsEven(n int) bool {\n return n%2 == 0\n}\n\nالطفرة النموذجية هي استبدال == بـ !=:
func IsEven(n int) bool {\n return n%2 != 0\n}\n\nإذا كان اختبارك يتحقق فقط من أن IsEven(2) لا يتعطل، ولكنه لا يتحقق من أن النتيجة هي true، فستنجو الطفرة. سينجح الاختبار، على الرغم من أن الدالة تعمل الآن بشكل معاكس تمامًا.
لماذا لا تكفي تغطية الكود البسيطة؟
\n\nيعتاد العديد من مطوري Go على الاعتماد على go test -cover. هذا مقياس مفيد، لكنه مضلل. لنلق نظرة على مثال:
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 result, err := Divide(10, 2)\n if err != nil {\n t.Fatal(err)\n }\n if result != 5 {\n t.Errorf("expected 5, got %d", result)\n }\n}\n\nالتغطية؟ 100% - تم تنفيذ جميع الأسطر. ولكن ماذا لو قام شخص ما عن طريق الخطأ باستبدال a / b بـ a * b؟ لن يلاحظ الاختبار ذلك، لأن 10 * 2 = 20، ونحن توقعنا 5. هل سيفشل الاختبار؟ لا، إذا لم نتحقق من القيمة الدقيقة.
سيقوم الاختبار الطفري في Go على الفور بكشف هذه المشكلة، عن طريق إنشاء طفرة مع عملية الضرب والتحقق مما إذا كان الاختبار سيفشل.
\n\nأدوات الاختبار الطفري في Go
\n\nالأداة الرئيسية هي go-mutesting. وهي مكتبة شائعة تقوم تلقائيًا بإنشاء الطفرات وتشغيل الاختبارات. التثبيت:
\n\ngo get github.com/zimmski/go-mutesting/cmd/go-mutesting\n\nهناك أيضًا بدائل مثل mutesting من شركة "Go Test It"، لكن go-mutesting هو المعيار الفعلي.
كيفية التشغيل
\n\nالتشغيل الأساسي:
\n\ngo-mutesting ./...\n\nسيجد هذا الأمر جميع ملفات _test.go في المشروع ويتحقق من أي الطفرات تنجو. يتم عرض النتيجة في وحدة التحكم مع تقرير مفصل.
مثال عملي: الاختبار الطفري على Go
\n\nلنقم بإنشاء حزمة بسيطة calculator تحتوي على دوال الجمع و