كانت اختباراتنا تصرخ ‘الذئب’: كيف قضينا على ‘الاختبارات المتقلبة’ (Flaky Tests) واستعدنا الثقة في خطوط الأنابيب؟

بتذكرها زي كأنها مبارح… كانت الساعة حوالي 11 بالليل، والفريق كله عيونه على شاشة الـ Slack. كنا بنستنى خط الأنابيب (CI/CD Pipeline) يعطينا الضوء الأخضر عشان نطلق الميزة الجديدة اللي سهرنا عليها أسابيع. القهوة بردت في فناجينها والتوتر كان سيد الموقف. فجأة، ظهر الإشعار اللي بنكرهه كلنا: Build Failed ❌.

فتحنا السجلات بسرعة، وإذا باختبار واحد، يتيم، هو سبب المصيبة. اختبار بسيط بتأكد من ظهور رسالة ترحيب للمستخدم. شغلناه على أجهزتنا 10 مرات، وفي كل مرة بنجح! “شو القصة يا جماعة؟” صرخ واحد من الشباب. أعدنا تشغيل الـ Pipeline مرة ثانية… فشل. مرة ثالثة… نجح! طيب نطلق؟ مين بضمن إنه ما يفشل كمان مرة عند العميل؟

هذا الموقف، يا جماعة الخير، هو تجسيد حي لقصة “الراعي الكذاب والذئب”. اختباراتنا كانت تصرخ “ذئب!” بشكل عشوائي، لدرجة إننا بطلنا نصدقها لما يكون في ذئب حقيقي (يعني Bug حقيقي). هذه هي “الاختبارات المتقلبة” أو “Flaky Tests”، اللعنة الصامتة التي تقتل ثقة الفريق في أهم أسلحته: الاختبار الآلي. في هذه المقالة، سأشارككم رحلتنا في القضاء على هذا الكابوس.

لماذا تصرخ اختباراتنا “الذئب”؟ – أشهر أسباب الاختبارات المتقلبة

الاختبار المتقلب هو اختبار ينجح أحيانًا ويفشل أحيانًا أخرى عند تشغيله على نفس الكود البرمجي وبدون أي تغييرات. هو ليس اختبارًا فاشلاً دائمًا (والذي يدل على وجود خطأ حقيقي)، وليس ناجحًا دائمًا. هذا التقلب هو ما يجعله خبيثًا. دعونا نفصّل الأسباب.

1. الاعتماد على الزمن والتوقيت (Asynchronicity)

هذا هو السبب رقم واحد بلا منازع. تطبيقات الويب الحديثة مليئة بالعمليات غير المتزامنة: طلبات API، الرسوم المتحركة (Animations)، تحديثات الواجهة التي تستغرق أجزاء من الثانية. الاختبار قد يحاول التحقق من وجود عنصر قبل أن يظهر على الشاشة فعلاً.

تخيل أنك تحاول التقاط صورة لشخص وهو يقفز في الهواء. إذا ضغطت على زر الكاميرا مبكرًا جدًا أو متأخرًا جدًا، فلن تحصل على الصورة المطلوبة. هذا ما يفعله اختبارك تمامًا.

مثال سيء (استخدام انتظار ثابت):


// لا تفعل هذا!
test('should display success message', async () => {
  await login('user', 'password');
  
  // انتظار أعمى لمدة ثانيتين، قد لا يكون كافيًا!
  await new Promise(r => setTimeout(r, 2000)); 
  
  const message = await screen.findByText('Welcome back!');
  expect(message).toBeVisible();
});

هذا الاختبار قد يفشل إذا استغرقت الشبكة أكثر من ثانيتين للرد.

2. البيئة المشتركة والحالة غير المعزولة (Shared State & Lack of Isolation)

مبدأ أساسي في الاختبار: يجب أن يكون كل اختبار مستقلاً بذاته، لا يعتمد على اختبار قبله ولا يؤثر على اختبار بعده. عندما تتشارك الاختبارات في قاعدة بيانات واحدة أو حالة (State) معينة دون تنظيفها، تحدث الكارثة.

  • اختبار (أ) يقوم بإنشاء مستخدم اسمه “Ahmad”.
  • اختبار (ب) يحاول إنشاء مستخدم جديد بنفس الاسم “Ahmad” ويفشل لأن الاسم موجود مسبقًا.

إذا تم تشغيل اختبار (ب) قبل (أ)، سينجحان كلاهما. ولكن إذا تم تشغيلهما بالترتيب أعلاه، سيفشل (ب). هذا هو التقلب بعينه.

3. الاعتماد على خدمات خارجية (External Dependencies)

هل يعتمد اختبارك على API حقيقي لجهة خارجية (مثل خدمة دفع أو خرائط جوجل)؟ ماذا لو كانت هذه الخدمة بطيئة في تلك اللحظة؟ أو لديها حد معين من الطلبات؟ أو كانت خارج الخدمة للصيانة؟ اختبارك سيفشل لأسباب لا علاقة لها بالكود الذي كتبته.

4. مشاكل في البنية التحتية للمنصة (Infrastructure Issues)

في بعض الأحيان، المشكلة ليست في الكود أو الاختبار، بل في البيئة التي تعمل فيها الاختبارات (CI/CD Runner). قد تكون موارد الجهاز (CPU, Memory) غير كافية، مما يجعل التطبيق أبطأ من المعتاد ويفشل الاختبار بسبب انتهاء مهلة الانتظار (Timeout). أو قد تحدث مشكلة لحظية في الشبكة داخل مركز البيانات.

كيف أسكتنا “الذئب”؟ – استراتيجيات عملية للقضاء على التقلب

بعد أن عانينا من هذا الكابوس، عقدنا العزم على وضع خطة محكمة. لم يكن الأمر سهلاً، ولكنه كان ضروريًا لاستعادة ثقتنا في خطوط الأنابيب.

1. مبدأ العزل أولاً: “كل واحد في حاله”

جعلنا هذا المبدأ مقدسًا. استخدمنا हुक्स (Hooks) مثل beforeEach و afterEach لضمان أن كل اختبار يبدأ ببيئة نظيفة وينتهي بتنظيف كل ما أحدثه.


// باستخدام مكتبة مثل Jest أو Vitest
describe('User Profile', () => {

  beforeEach(async () => {
    // قبل كل اختبار، قم بمسح قاعدة البيانات وإعادة ملئها ببيانات أولية معروفة
    await db.clear();
    await db.seed({ users: [ { id: 1, name: 'Omar' } ] });
  });

  afterEach(async () => {
    // يمكنك أيضًا التنظيف بعد كل اختبار، على الرغم من أن التنظيف قبله أكثر أمانًا
    await db.clear();
  });

  test('should display user name', () => {
    // ...
  });

  test('should allow user to update their name', () => {
    // ...
  });
});

2. التعامل مع “الزمن الغدار”: الانتظار بذكاء

توقفنا تمامًا عن استخدام “الانتظار الثابت” (Static Waits) مثل `setTimeout`. بدلاً من ذلك، اعتمدنا على “الانتظار الصريح” (Explicit Waits) الذي توفره مكتبات الاختبار الحديثة.

مثال جيد (استخدام انتظار ذكي مع Playwright):


import { test, expect } from '@playwright/test';

test('should display success message', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Username').fill('user');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Log in' }).click();
  
  // لا تنتظر لوقت محدد!
  // انتظر حتى يصبح العنصر الذي تريده مرئيًا على الشاشة.
  // Playwright سيعيد المحاولة تلقائيًا لعدة ثوانٍ.
  await expect(page.getByText('Welcome back!')).toBeVisible();
});

هنا، نحن لا نأمر الاختبار بالانتظار، بل نصف له ما الذي يجب أن ينتظره. هذا هو الفارق الجوهري.

3. فن “التمثيل”: عزل الخدمات الخارجية (Mocking & Stubbing)

لكل خدمة خارجية نعتمد عليها، قمنا بإنشاء “بديل” أو “ممثل” (Mock). هذا الممثل يحاكي سلوك الخدمة الحقيقية ولكنه يعمل بشكل فوري وموثوق تحت سيطرتنا الكاملة.

مثال بسيط على Mocking باستخدام Jest:


import { getUserProfile } from './api';
import axios from 'axios';

// إخبار Jest بأننا سنتحكم في مكتبة axios
jest.mock('axios');

test('fetches user profile successfully', async () => {
  const userData = { name: 'Abu Omar', role: 'Developer' };
  
  // إعداد الـ "ممثل" ليرجع بيانات محددة عند استدعائه
  axios.get.mockResolvedValue({ data: userData });

  const profile = await getUserProfile(123);

  expect(profile).toEqual(userData);
  expect(axios.get).toHaveBeenCalledWith('/users/123'); // نتأكد من أنه تم استدعاؤه بالطريقة الصحيحة
});

بهذه الطريقة، أصبح اختبارنا سريعًا ومستقرًا، ولا يعتمد على توفر خدمة خارجية حقيقية.

4. عندما يفشل كل شيء: إعادة المحاولة بذكاء (Smart Retries)

هذه أداة يجب استخدامها بحذر شديد. بعض أدوات CI/CD (مثل Cypress Dashboard أو Playwright) تسمح بإعادة تشغيل الاختبارات الفاشلة تلقائيًا. هذا يمكن أن يكون حلاً مؤقتًا جيدًا لإبقاء خط الأنابيب “أخضر” بينما تعمل على إصلاح السبب الجذري للاختبار المتقلب.

نصيحة من الختيار: إعادة المحاولة مثل المسكّن، تخفف الألم ولكنها لا تعالج المرض. استخدمها لكسب الوقت، ولكن لا تتجاهل المشكلة الأصلية. قم بوضع علامة (Tag) على هذه الاختبارات (`@flaky`) لمتابعتها وإصلاحها لاحقًا.

5. “دفتر الملاحظات”: المراقبة والتصنيف

خصصنا لوحة (Dashboard) لمراقبة صحة اختباراتنا. أي اختبار يفشل بشكل متقلب يتم عزله في قائمة خاصة (Flaky Tests Quarantine). الفريق يجتمع أسبوعيًا لمراجعة هذه القائمة وتوزيع مهام إصلاحها. هذا يمنع الاختبارات المتقلبة من تعطيل العمل اليومي للفريق بأكمله.

الخلاصة: من الشك إلى اليقين 🎯

الاختبارات المتقلبة ليست مجرد مشكلة تقنية، بل هي مشكلة ثقافية. إنها تآكل ببطء ثقة الفريق في شبكة الأمان الخاصة به، وتجعل عمليات النشر مرعبة ومحفوفة بالمخاطر. القضاء عليها يتطلب مجهودًا جماعيًا، وتغييرًا في طريقة تفكيرنا وكتابتنا للاختبارات.

تذكر دائمًا أن الهدف من الاختبار الآلي ليس مجرد الحصول على علامة صح خضراء، بل الحصول على الثقة. الثقة في أن الكود الذي كتبته يعمل كما هو متوقع، والثقة في أنك تستطيع النشر في أي وقت بأمان. عندما تتوقف اختباراتك عن الصراخ “ذئب!” بشكل كاذب، ستتمكن أخيرًا من سماعها بوضوح عندما يكون هناك خطر حقيقي، وهذا هو ما يصنع الفرق بين فريق يتقدم بثقة وفريق يعيش في خوف دائم من الفشل.

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

البنية التحتية وإدارة السيرفرات

ميزانيات الخطأ (Error Budgets): كيف أنهت كابوس مكالمات منتصف الليل وأنقذتنا من الإرهاق؟

كنا غارقين في مكالمات طوارئ ليلية لا تنتهي، فريق منهك والمنتج على المحك. في هذه المقالة، أشارككم قصة كيف أنقذنا مفهوم "ميزانيات الخطأ" (Error Budgets)...

30 مايو، 2026 قراءة المزيد
ادارة الفرق والتنمية البشرية

كانت اجتماعاتنا الفردية استجواباً صامتاً: كيف حولنا الـ 1-on-1 من تقرير حالة ممل إلى محرك لنمو الفريق؟

أشارككم تجربتي كقائد فريق تقني في تحويل الاجتماعات الفردية (1-on-1s) من جلسات استجواب مملة إلى محادثات مثمرة تساهم في بناء الثقة وتطوير الفريق. هذه المقالة...

30 مايو، 2026 قراءة المزيد
أدوات وانتاجية

كانت أصابعي تصرخ من التكرار: كيف أنقذتني ‘مقتطفات الشفرة’ (Code Snippets) من جحيم كتابة Boilerplate؟

أشارككم قصتي مع التكرار الممل في البرمجة وكيف غيرت "مقتطفات الشفرة" (Code Snippets) طريقة عملي تماماً. دليل عملي من مبرمج فلسطيني لزيادة إنتاجيتك والتخلص من...

30 مايو، 2026 قراءة المزيد
أتمتة العمليات

كانت تبعياتنا قنبلة موقوتة: كيف أنقذنا ‘التحديث الآلي للتبعيات’ من جحيم الثغرات الأمنية المنسية؟

أشارككم قصة حقيقية عن ليلة كادت فيها ثغرة أمنية في إحدى المكتبات المنسية أن تدمر مشروعنا بالكامل. اكتشفوا معنا كيف تحولنا من الفوضى إلى الأمان...

30 مايو، 2026 قراءة المزيد
نصائح برمجية

كانت شفرتنا هرمًا من الهلاك: كيف أنقذتنا ‘شروط الحماية’ (Guard Clauses) من جحيم الـ if/else المتداخلة؟

في هذه المقالة، أشارككم قصة حقيقية من مسيرتي كمبرمج عن المعاناة مع الشفرات المتداخلة "هرم الهلاك". سنتعلم كيف تنقذنا تقنية "شروط الحماية" (Guard Clauses) من...

30 مايو، 2026 قراءة المزيد
​معمارية البرمجيات

كانت خدماتنا متلاصقة كالغراء: كيف أنقذتنا ‘المعمارية الموجهة بالأحداث’ (EDA) من جحيم الاقتران المحكم؟

أنا أبو عمر، وهذا المقال ليس مجرد شرح تقني، بل هو قصة حقيقية من الخنادق البرمجية. سأروي لكم كيف كنا نغرق في بحر "الاقتران المحكم"،...

30 مايو، 2026 قراءة المزيد
ذكاء اصطناعي

كانت نماذجنا تموت ببطء: كيف أنقذنا “انحراف النموذج” (Model Drift) من جحيم التنبؤات الفاسدة؟

في عالم الذكاء الاصطناعي، نماذجنا ليست منحوتات حجرية، بل كائنات حية تتنفس البيانات. أشارككم قصة حقيقية عن "انحراف النموذج" (Model Drift)، هذا الشبح الذي كاد...

30 مايو، 2026 قراءة المزيد
البودكاست