اختبار العقود (Contract Testing): طوق النجاة في عالم الخدمات المصغرة الفوضوي

يا جماعة الخير، السلام عليكم ورحمة الله وبركاته.

أذكرها وكأنها البارحة. كانت ليلة خميس، والساعة قاربت منتصف الليل. الفريق كله “متأهب” لإطلاق ميزة جديدة انتظرها المستخدمون طويلاً. الأجواء كانت مشحونة بالتوتر والحماس، وكاسات الشاي والقهوة “رايحة جاي”. ضغطنا زر النشر (Deploy)، والكل يراقب الشاشات بتركيز شديد. ثوانٍ مرت وكأنها دهر، ثم ظهرت الكارثة على شاشة المراقبة: سلسلة من الأخطاء باللون الأحمر القاني. فشل، فشل، فشل!

بدأ الهرج والمرج، وصار كل فريق يرمي التهمة على الآخر. فريق “الواجهات الأمامية” يقول إن خدمة “الطلبات” لا تستجيب. فريق “الطلبات” يصرخ بأن المشكلة من خدمة “المستخدمين” التي غيرت شيئًا ما دون إعلامهم. وبعد ساعة من “التنبيش” في السجلات (Logs)، اكتشفنا المصيبة: فريق خدمة المستخدمين، في خضم حماسه لتحسين الأداء، غيّر اسم حقل في الاستجابة من userId إلى user_id. تغيير بسيط، حرف واحد، لكنه كان كفيلاً بإسقاط النظام بأكمله كأحجار الدومينو.

تلك الليلة، وبعد أن أصلحنا المشكلة “يدوياً” وأطلقنا الميزة بنجاح (بعد جرعة توتر لا أحسد عليها أحداً)، عقدنا اجتماعاً طارئاً. كان السؤال واحداً: كيف نمنع هذه “المعمعة” من الحدوث مرة أخرى؟ ومن هنا، بدأت رحلتنا مع ما يسمى بـ “اختبار العقود” (Contract Testing)، التقنية التي أعادت السلام والثقة إلى بيئة خدماتنا المصغرة.

ما هو اختبار العقود (Contract Testing)؟ ببساطة شديدة

تخيل أنك تبني بيتاً، وأنت مسؤول عن تركيب الأبواب (خدمة مستهلكة – Consumer). وهناك نجار يصنع لك هذه الأبواب (خدمة مزودة – Provider). قبل أن يبدأ النجار عمله، تتفقان على “عقد”: طول الباب، عرضه، سُمكه، مكان ثقب المفتاح. هذا هو العقد.

اختبار العقود هو ببساطة التأكد من أن كلا الطرفين يلتزمان بهذا الاتفاق:

  • أنت (المستهلك): تتأكد أن الإطار الذي بنيته في الحائط يطابق أبعاد الباب المتفق عليها في العقد.
  • النجار (المزود): يتأكد أن الباب الذي صنعه يطابق تماماً المواصفات الموجودة في العقد.

الأهم من ذلك، كل منكما يختبر الجزء الخاص به بشكل مستقل. أنت لا تحتاج إلى انتظار النجار ليحضر الباب الفعلي لتقيس إطارك، وهو لا يحتاج إلى بيتك ليقيس الباب. كل ما تحتاجانه هو “العقد” المشترك بينكما. في عالم البرمجيات، هذا العقد هو ملف يصف التفاعلات المتوقعة بين خدمتين (Request/Response).

لماذا نحتاج اختبار العقود؟ أليس اختبار التكامل (Integration Testing) كافيًا؟

هذا سؤال مشروع جداً. كنا نعتمد بشكل كبير على اختبارات التكامل الشاملة (End-to-End Integration Tests)، لكنها كانت كابوساً حقيقياً. دعوني أوضح الفرق:

اختبار التكامل التقليدي (Integration Testing):

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

اختبار العقود (Contract Testing):

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

باختصار، اختبار التكامل يصرخ في وجهك: “هناك خطأ ما في مكان ما بين هذه الخدمات العشر!”. أما اختبار العقود فيهمس في أذنك بهدوء: “انتبه، خدمة X تتوقع منك حقلاً اسمه userId، لكنك أعدت لها user_id. أصلح الأمر قبل أن يصبح مشكلة.”

كيف يعمل اختبار العقود؟ دورة حياة العقد (باستخدام Pact)

أشهر أداة في هذا المجال هي “Pact”. لن نخوض في تفاصيلها المعقدة، بل سنركز على الفكرة الأساسية التي تنقسم إلى خطوتين رئيسيتين:

الخطوة الأولى: من جانب المستهلك (Consumer)

في هذه المرحلة، يقوم فريق الخدمة المستهلكة (مثلاً، خدمة الطلبات order-service) بتعريف توقعاته من الخدمة المزودة (مثلاً، خدمة المستخدمين user-service).

  1. كتابة الاختبار: يكتب المطور اختباراً يصف كيف يتوقع أن تكون استجابة user-service. على سبيل المثال: “عندما أطلب المستخدم رقم 1، أتوقع استجابة ناجحة (200 OK) تحتوي على جسم JSON به id (رقم) و name (نص)”.
  2. إنشاء مزود وهمي (Mock Provider): تقوم أداة Pact بإنشاء خادم وهمي بناءً على هذه التوقعات.
  3. تشغيل اختبارات المستهلك: تعمل اختبارات order-service ضد هذا الخادم الوهمي. إذا كانت الخدمة تتعامل مع الاستجابة الوهمية بشكل صحيح، ينجح الاختبار.
  4. توليد العقد (Pact File): إذا نجح الاختبار، تقوم Pact بتوليد ملف JSON يسمى “العقد”. هذا الملف هو توثيق دقيق للتوقعات التي تم اختبارها.

الخطوة الثانية: من جانب المزود (Provider)

الآن، ينتقل العقد إلى فريق الخدمة المزودة (user-service).

  1. الحصول على العقد: يحصل المزود على ملف العقد (إما مباشرة أو عبر خادم مركزي يسمى Pact Broker).
  2. إعادة تشغيل الطلبات: تقوم أداة Pact بتشغيل الطلبات المذكورة في العقد ضد الخدمة المزودة الحقيقية.
  3. التحقق من الاستجابات: تقارن Pact الاستجابات الفعلية من user-service مع الاستجابات المتوقعة والموثقة في العقد.
  4. النتيجة:
    • إذا تطابقت الاستجابات، فهذا يعني أن المزود يفي بالعقد. نجاح!
    • إذا لم تتطابق (مثلاً، اسم الحقل تغير أو نوع البيانات اختلف)، يفشل الاختبار. وهنا يعرف فريق user-service فوراً أن التغيير الذي أجروه سيكسر التكامل مع order-service، ويمكنهم إصلاحه قبل أن يسبب أي كارثة.

مثال عملي بسيط (Node.js و Pact.js)

لنفترض أن لدينا خدمة order-service (المستهلك) تريد جلب بيانات مستخدم من خدمة user-service (المزود).

1. اختبار المستهلك (في مشروع order-service)

نكتب اختباراً باستخدام Jest و Pact. هذا الاختبار يصف التوقع ويولد ملف العقد.


// order-service/user.consumer.test.js
import { Pact } from '@pact-foundation/pact';
import { fetchUser } from './api'; // دالة تقوم بطلب البيانات من خدمة المستخدمين

const provider = new Pact({
  consumer: 'OrderService',
  provider: 'UserService',
  port: 1234,
});

describe('API Pact test for UserService', () => {
  beforeAll(() => provider.setup());
  afterAll(() => provider.finalize());

  describe('given a user exists', () => {
    it('returns the user data', async () => {
      // 1. تعريف التوقعات (العقد)
      await provider.addInteraction({
        state: 'a user with id 1 exists',
        uponReceiving: 'a request to get user 1',
        withRequest: {
          method: 'GET',
          path: '/users/1',
        },
        willRespondWith: {
          status: 200,
          headers: { 'Content-Type': 'application/json; charset=utf-8' },
          body: {
            id: 1,
            name: 'أبو عمر', // نتوقع اسمًا من نوع نص
            email: 'abu.omar@example.com'
          },
        },
      });

      // 2. تشغيل الكود الفعلي الذي يقوم بالطلب
      const user = await fetchUser(1); // هذه الدالة تستخدم الرابط http://localhost:1234

      // 3. التأكد من أن الكود يتعامل مع الاستجابة المتوقعة
      expect(user.name).toBe('أبو عمر');
    });
  });
});

عند تشغيل هذا الاختبار، سيتم إنشاء ملف OrderService-UserService.json في مجلد pact/pacts. هذا هو العقد.

2. التحقق من جانب المزود (في مشروع user-service)

الآن، نأخذ ملف العقد ونستخدمه للتحقق من المزود.


// user-service/provider.verification.test.js
import { Verifier } from '@pact-foundation/pact';
import path from 'path';
import server from './server'; // الخادم الفعلي لخدمة المستخدمين

const port = 5000;
let app;

// بدء الخادم قبل الاختبارات
beforeAll(() => {
    app = server.listen(port);
});

// إغلاق الخادم بعد الاختبارات
afterAll((done) => {
    app.close(done);
});

describe('Pact Verification', () => {
  it('validates the expectations of OrderService', () => {
    const opts = {
      provider: 'UserService',
      providerBaseUrl: `http://localhost:${port}`, // رابط الخدمة الحقيقية
      pactUrls: [
        path.resolve(process.cwd(), '../order-service/pacts/orderservice-userservice.json') // مسار ملف العقد
      ],
    };

    return new Verifier(opts).verifyProvider();
  });
});

عند تشغيل هذا الاختبار، ستقوم Pact بمحاكاة الطلب (GET /users/1) على الخادم الحقيقي لـ user-service. إذا كانت الاستجابة الفعلية مطابقة لما هو موجود في ملف العقد، ينجح الاختبار. إذا قام مطور user-service بتغيير name إلى fullName، فسيفشل هذا الاختبار فوراً، وسيعرف أن عليه إما التراجع عن التغيير أو التنسيق مع فريق order-service.

نصائح من خبرة أبو عمر

  • ابدأ صغيرًا وبشكل استراتيجي: لا تحاول تطبيق هذا على كل خدماتك دفعة واحدة. اختر خدمتين حيويتين بينهما الكثير من المشاكل، وأثبت نجاح الفكرة. ثم توسع تدريجياً.
  • العقد يصف “النية” وليس “كل شيء”: يجب أن يركز العقد فقط على الحقول التي يحتاجها المستهلك فعلاً. إذا كانت استجابة المزود تحتوي على 20 حقلاً والمستهلك يستخدم 3 فقط، يجب أن يحتوي العقد على هذه الثلاثة فقط. هذا يجعل العقود أقل هشاشة وأكثر مرونة.
  • اجعلها جزءاً لا يتجزأ من الـ CI/CD Pipeline: القوة الحقيقية لاختبار العقود تظهر عندما يتم تشغيلها تلقائياً مع كل عملية `push` أو `pull request`. يجب أن يفشل الـ build إذا تم كسر عقد ما. هذا يفرض الجودة ويمنع المشاكل قبل وصولها لمرحلة الدمج.
  • استخدم Pact Broker (أو ما يماثله): عندما تكبر المنظومة، يصبح تبادل ملفات العقود يدوياً أمراً مرهقاً. Pact Broker هو خادم مركزي لإدارة العقود وإصداراتها، ويعطيك رؤية واضحة حول “من يستهلك ماذا” ومن “يمكن نشره بأمان”.
  • اختبار العقود لا يغني عن بقية الاختبارات: تذكر هرم الاختبار. اختبار العقود ليس بديلاً عن اختبارات الوحدة (Unit Tests) التي تختبر منطق العمل، ولا عن بضعة اختبارات تكامل شاملة (E2E) للتأكد من أن التدفق العام يعمل كما هو متوقع. إنه يحل مشكلة محددة جداً بكفاءة عالية.

الخلاصة 🚀

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

قد يبدو الإعداد في البداية معقداً، لكن العائد على المدى الطويل هائل: تقليل هائل في أخطاء التكامل، سرعة أكبر في النشر، وثقة أكبر في التغييرات التي نجريها. الأهم من كل ذلك، نوم هانئ في ليالي الإطلاق.

يلا يا جماعة، شدوا حيلكم، وخلينا نبني برمجيات “مرتبة” ونظيفة. ويعطيكم ألف عافية.

أبو عمر

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

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

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

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

آخر المدونات

ادارة الفرق والتنمية البشرية

مراجعة الكود كانت حقل ألغام: كيف حوّلنا الصراع إلى تعاون بـ “المراجعات القائمة على التعاطف”؟

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

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

كانت بياناتنا تتغير بغدر: كيف أنقذتنا ‘الكائنات غير القابلة للتغيير’ (Immutability) من جحيم الآثار الجانبية؟

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

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

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

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

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

كانت إجابات نموذجنا من وحي الخيال: كيف أنقذنا البحث المعزز بالتوليد (RAG) من جحيم الهلوسة؟

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

18 مايو، 2026 قراءة المزيد
خوارزميات

كانت شخصياتنا في اللعبة تسير في حوائط: كيف أنقذتنا خوارزمية A* من جحيم المسارات الغبية؟

أشارككم قصة من أيام تطوير الألعاب، حين كانت شخصياتنا تتصرف بغباء وتصطدم بالحوائط. سأشرح لكم بالتفصيل كيف أنقذتنا خوارزمية A* (نجمة إيه)، وكيف يمكنكم استخدامها...

17 مايو، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

كانت واجهاتنا جزرًا معزولة: كيف أنقذنا ‘نظام التصميم’ من جحيم الفوضى البصرية؟

أشارككم قصة حقيقية من قلب المعركة البرمجية، كيف انتقلنا من فوضى الواجهات والتصاميم المتضاربة إلى نظام متناغم وموحّد. هذه رحلتنا في بناء "نظام تصميم" (Design...

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