لماذا يخفي Go نتائج الاختبارات؟ (وكيف يعطل حياتك)
\n\nتخيل: أنت تكتب بلغة Go، وتشغّل go test، وترى علامة خضراء... لكن شيئًا ما ليس صحيحًا. لقد أصلحت خطأً، وتشغّل الاختبارات مرة أخرى — وترى الضوء الأخضر مرة أخرى. لكن الخطأ ما زال موجودًا! هل هذا مألوف؟ مرحبًا بك في عالم تخزين نتائج الاختبارات في Go.
Go هي لغة تحب السرعة. لتجنب تشغيل نفس الاختبارات عشر مرات، يقوم المترجم بتخزين النتائج مؤقتًا. يبدو هذا مريحًا، حتى تواجه موقفًا تبدأ فيه ذاكرة التخزين المؤقت لاختبارات Go في العمل ضدك. هذا يؤثر بشكل خاص على المطورين الذين يعملون مع اختبارات التكامل، ونظام الملفات، أو واجهات برمجة التطبيقات الخارجية.
\n\nفي هذه المقالة، سنحلل طريقتين لحل المشكلة: force retesting (إعادة التشغيل القسرية) و تعطيل ذاكرة التخزين المؤقت للاختبارات بالكامل. ستتعرف على متى تستخدم كل منهما، وستحصل على أمثلة كود عملية.
\n\nكيف يعمل تخزين نتائج الاختبارات في Go
\n\nقبل أن نكسر شيئًا، دعنا نفهم كيف يعمل. يقوم Go بتخزين نتائج الاختبارات بناءً على تجزئة (hash) من:
\n- \n
- الكود المصدري (ملفات
.go) \n - ملفات الاختبار \n
- متغيرات البيئة \n
- أعلام التجميع \n
إذا لم يتغير شيء — يعرض Go ببساطة النتيجة المخزنة. سريع؟ نعم. خطير؟ بالتأكيد! تخيل أن اختبارك يعتمد على الوقت، أو الأرقام العشوائية، أو خدمة خارجية. ستقول ذاكرة التخزين المؤقت: «كل شيء على ما يرام!»، بينما في الواقع كان الاختبار سيفشل منذ زمن.
\n\n// مثال لاختبار سيكون أخضر بشكل خاطئ بسبب ذاكرة التخزين المؤقت\nfunc TestCurrentTime(t *testing.T) {\n now := time.Now()\n if now.Year() != 2024 {\n t.Errorf("يجب أن يكون العام 2024، ولكن تم الحصول على %d", now.Year())\n }\n}\n\nشغّل هذا الاختبار مرة واحدة — سينجح. هل غيرت العام إلى 2025 في الكود؟ ستظهر ذاكرة التخزين المؤقت النتيجة القديمة. وستتساءل لماذا يفشل CI بينما كل شيء «يعمل» محليًا.
\n\nالطريقة 1: Force retesting (إعادة التشغيل القسرية)
\n\nالطريقة الأكثر شيوعًا لمكافحة ذاكرة التخزين المؤقت هي العلم -count=1. هذا هو force retesting في أنقى صوره. إنه يخبر Go: «لا تثق في ذاكرة التخزين المؤقت، أعد تشغيل الاختبار من جديد».
// تشغيل مع إعادة التشغيل القسرية\ngo test -count=1 ./...\n\nلماذا -count=1 وليس -count=0؟ لأن -count يحدد عدد مرات تشغيل الاختبار. 1 هي القيمة الدنيا التي تعطل ذاكرة التخزين المؤقت. 0 ببساطة لن يشغل أي شيء.
متى تستخدم force retesting
\n- \n
- اختبارات التكامل التي تتصل بقاعدة بيانات أو API \n
- اختبارات ببيانات عشوائية (اختبارات التشويش fuzz، توليد UUID) \n
- اختبارات تعتمد على الوقت (المهلات، التواريخ) \n
- التطوير المحلي عندما تغير الكود بشكل متكرر \n
مثال: اختبار تكامل مع force retesting
\n\npackage db_test\n\nimport (\n "testing"\n "time"\n)\n\nfunc TestDatabaseConnection(t *testing.T) {\n // هذا الاختبار يعتمد على خادم حي\n // بدون -count=1 قد يظهر نتيجة قديمة\n db := connectToDatabase()\n if err := db.Ping(); err != nil {\n t.Fatalf("فشل الاتصال: %v", err)\n }\n \n // محاكاة تأخير زمني\n time.Sleep(100 * time.Millisecond)\n if err := db.Query("SELECT 1"); err != nil {\n t.Errorf("لم يتم تنفيذ الاستعلام: %v", err)\n }\n}\n\nنشغّل: go test -count=1 -v ./db/. الآن كل تشغيل هو تشغيل حقيقي. لكن هناك فارق بسيط: force retesting لا يحل المشكلة بشكل جذري. إذا نسيت العلم — ستعود ذاكرة التخزين المؤقت.
الطريقة 2: التعطيل الكامل لذاكرة التخزين المؤقت للاختبارات
\n\nالخيار الثاني — جذري. أنت ببساطة تخبر Go: «لا تخزن أي شيء». يتم ذلك من خلال متغير البيئة GOCACHE=off أو العلم -count=1