وداعاً لكابوس “الـ API انكسر!”: تجربتي مع اختبار العقود (Contract Testing) في عالم الخدمات المصغرة

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

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

نزل الخبر عليّ زي الصاعقة. كيف يعني؟ اختبرنا كل شيء! فتحنا اللابتوبات بسرعة البرق، وبدأنا رحلة البحث عن السبب. بعد ساعة من التوتر وحرق الأعصاب، اكتشفنا المشكلة. فريق “خدمة الطلبات” (Orders Service) كان قد أجرى تحديثاً بسيطاً على الـ API الخاص به، غيروا اسم حقل من "userId" إلى "user_id". تحديث بسيط جداً، لكنه كان كافياً ليكسر “خدمة المصادقة” (Authentication Service) بالكامل، لأنها كانت تعتمد على الاسم القديم للحقل.

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

ما هو أصل المشكلة؟ جحيم التكامل في الخدمات المصغرة

الخدمات المصغرة معمارية رائعة، كلنا بنحبها. بتعطينا استقلالية للفرق، سرعة في التطوير، وقدرة على التوسع (Scalability). لكن مع كل هاي الميزات، بيجي تحدي كبير اسمه “التكامل” (Integration).

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

الطرق التقليدية للاختبار ما كانت كافية:

  • اختبارات الوحدة (Unit Tests): ممتازة، لكنها تختبر كل خدمة بمعزل عن الأخرى. بتضمن إن المبنى نفسه من جوا شغال، بس ما بتضمن إن الجسر اللي بوصله شغال.
  • الاختبارات الشاملة (End-to-End Tests): هاي الاختبارات بتشغّل النظام كله وبتختبره. نظرياً ممتازة، لكن عملياً كابوس في بيئة الخدمات المصغرة. بطيئة جداً، هشة (بتنكسر لأتفه الأسباب)، وصيانتها مكلفة جداً، خصوصاً لما يكون عندك عشرات أو مئات الخدمات.

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

الحل ليس سحراً، بل هو منطق سليم: لنتعرف على اختبار العقود (Contract Testing)

اختبار العقود هو أسلوب يضمن أن خدمتين (مستهلك ومزود) يمكنهما التواصل مع بعضهما البعض. الفكرة بسيطة جداً وتقوم على مفهوم “العقد”.

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

هذا العقد بيتم مشاركته مع فريق “المستشفى” (المزود – Provider). الآن، كل فريق بيختبر بشكل مستقل:

  1. فريق المستهلك (الإسعاف): بيعمل محاكاة (Mock) للمستشفى بناءً على العقد، وبيتأكد إن سياراته قادرة تتعامل مع هذا التوقع.
  2. فريق المزود (المستشفى): بيستخدم نفس العقد عشان يتأكد إن الجسر اللي بناه فعلاً مطابق للمواصفات اللي بيحتاجها فريق الإسعاف.

إذا قرر فريق المستشفى يغير مكان المدخل، اختبار العقد عندهم رح يفشل قبل ما يعملوا نشر للكود، ورح يوصلهم تنبيه: “انتبه! هذا التغيير سيكسر التكامل مع فريق الإسعاف!”. وهيك بنكون اكتشفنا المشكلة مبكراً وبشكل آلي. هذا هو جوهر الـ Consumer-Driven Contract Testing.

كيف يعمل هذا على أرض الواقع؟ مثال عملي باستخدام Pact

Pact هي أشهر أداة لتطبيق اختبار العقود. خلينا نطبقها على مشكلتنا الأصلية: خدمة الواجهة الأمامية (المستهلك) التي تطلب بيانات المستخدم من خدمة المستخدمين (المزود).

السيناريو: الواجهة الأمامية (Consumer) تريد عرض اسم المستخدم وعمره، فتطلب من الـ Users-API (Provider) بيانات المستخدم صاحب الـ ID رقم 10.

الخطوة الأولى: في جهة المستهلك (Consumer Side)

في كود الواجهة الأمامية، سنكتب اختباراً باستخدام مكتبة pact-js. هذا الاختبار سيقوم بثلاثة أشياء: تعريف العقد، تشغيل كود الواجهة الذي يتعامل مع الـ API، ثم توليد ملف العقد (ملف JSON).

// consumer.spec.js (باستخدام Jest و pact-js)
import { Pact } from '@pact-foundation/pact';
import { eachLike } from '@pact-foundation/pact/src/dsl/matchers';
import { fetchUser } from './apiClient'; // هذا هو الكود الفعلي الذي نريد اختباره

// 1. إعداد بيئة الاختبار لـ Pact
const provider = new Pact({
  consumer: 'Website-Frontend', // اسم المستهلك
  provider: 'Users-API',      // اسم المزود
  port: 1234,                 // المنفذ الذي سيعمل عليه المزود الوهمي
});

describe('API Pact test', () => {
  beforeAll(() => provider.setup()); // تشغيل المزود الوهمي
  afterEach(() => provider.verify()); // التحقق من أن كل التفاعلات المتوقعة قد حدثت
  afterAll(() => provider.finalize()); // إيقاف المزود الوهمي وتوليد ملف العقد

  describe('getting a user by ID', () => {
    it('should return a user object', async () => {
      // 2. تعريف العقد (التفاعل المتوقع)
      await provider.addInteraction({
        state: 'a user with ID 10 exists', // الحالة التي يجب أن يكون عليها المزود
        uponReceiving: 'a request to get user 10', // وصف الطلب
        withRequest: {
          method: 'GET',
          path: '/users/10',
        },
        willRespondWith: {
          status: 200,
          headers: { 'Content-Type': 'application/json; charset=utf-8' },
          body: {
            id: 10,
            name: "Abu Omar", // نتوقع حقلاً اسمه name
            age: 42,         // وآخر اسمه age
          },
        },
      });

      // 3. تشغيل الكود الفعلي الذي يقوم بالطلب
      const user = await fetchUser(10); // fetchUser هو الذي ينادي http://localhost:1234/users/10

      // التأكد من أن الكود الخاص بنا تعامل مع الاستجابة بشكل صحيح
      expect(user.name).toBe("Abu Omar");
    });
  });
});

عند تشغيل هذا الاختبار، سيقوم Pact بإنشاء ملف JSON اسمه Website-Frontend-Users-API.json. هذا هو العقد الرسمي بين الخدمتين.

الخطوة الثانية: العقد الوسيط (Pact Broker)

ملف العقد هذا يجب أن يكون متاحاً للطرفين. أفضل طريقة هي استخدام ما يسمى بالـ “Pact Broker”. هو عبارة عن خادم مركزي لتخزين العقود وإصداراتها ومشاركة نتائج التحقق. عند كل تغيير في كود المستهلك، يتم نشر نسخة جديدة من العقد على الـ Broker. هذا يسمح بالتشغيل الآلي للعملية بأكملها ضمن خط أنابيب التكامل المستمر (CI/CD Pipeline).

الخطوة الثالثة: في جهة المزود (Provider Side)

الآن، حان دور فريق الـ Users-API. في الـ CI/CD pipeline الخاص بهم، سيقومون بسحب أحدث العقود من الـ Pact Broker التي تخصهم. ثم، سيقومون بتشغيل اختبار “تحقق” (Verification).

هذا الاختبار سيقوم بالتالي:

  1. تشغيل الـ API الفعلي الخاص بهم.
  2. سيقوم Pact “بإعادة تشغيل” الطلبات الموجودة في العقد (مثل GET /users/10) ضد الـ API الفعلي.
  3. يقارن الاستجابة الفعلية من الـ API مع الاستجابة المتوقعة في العقد.
// provider.spec.js
const { Verifier } = require('@pact-foundation/pact');
const server = require('./provider'); // ملف تشغيل خادم Express الخاص بنا

// ... كود تشغيل الخادم قبل الاختبار وإيقافه بعده

describe('Pact Verification', () => {
  it('validates the expectations of Website-Frontend', () => {
    const opts = {
      provider: 'Users-API',
      providerBaseUrl: 'http://localhost:8080', // عنوان الـ API الفعلي
      
      // جلب العقود من الـ Broker
      pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
      pactBrokerToken: process.env.PACT_BROKER_TOKEN,
      publishVerificationResult: true, // نشر نتيجة التحقق إلى الـ Broker
      providerVersion: '1.0.1', // إصدار المزود الحالي
    };

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

الآن، تخيل لو أن مطوراً في فريق الـ Users-API قام بتغيير حقل name إلى fullName. عند تشغيل هذا الاختبار، سيقوم Pact بإرسال طلب GET /users/10، وسيحصل على استجابة تحتوي على { "id": 10, "fullName": "Abu Omar", "age": 42 }. سيقارنها Pact مع العقد الذي يتوقع وجود حقل name. هنا، سيفشل الاختبار! وسيعرف المطور أنه على وشك كسر الواجهة الأمامية قبل أن يصل الكود إلى مرحلة الإنتاج. وهذه هي القوة الحقيقية لاختبار العقود.

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

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

الخلاصة: من الفوضى إلى الثقة 🚀

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

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

يلا يا جماعة، خلينا نبني أنظمة قوية وموثوقة مع بعض. وبالتوفيق للجميع!

أبو عمر

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

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

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

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

آخر المدونات

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

كان الصمت يصم الآذان: كيف أنقذت ‘السلامة النفسية’ فريقنا من جحيم الأخطاء المدفونة؟

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

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

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

بصفتي أبو عمر، مبرمج فلسطيني، كنت أغرق في فوضى ملاحظاتي الرقمية. في هذه المقالة، أسرد لكم كيف انتشلني تطبيق Obsidian من هذا الجحيم، وساعدني في...

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

كان تاريخ قراراتنا ضبابياً: كيف أنقذتنا ‘سجلات القرارات المعمارية’ (ADRs) من جحيم الأسئلة المتكررة؟

في عالم تطوير البرمجيات سريع الخطى، غالباً ما ننسى "لماذا" اتخذنا قراراً معمارياً معيناً. أشارككم تجربتي كـ "أبو عمر" وكيف أنقذتنا سجلات القرارات المعمارية (ADRs)...

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

كانت حساباتنا تتكرر إلى ما لا نهاية: كيف أنقذتنا ‘البرمجة الديناميكية’ من جحيم التعقيد الأسي؟

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

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

كانت واجهاتنا خليطاً فوضوياً: كيف أنقذنا ‘نظام التصميم’ من جحيم عدم الاتساق؟

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

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