خدماتي كانت تتحدث لغات مختلفة: كيف أنقذني اختبار العقود (Contract Testing) من جحيم التكامل الهش؟

مقدمة من قلب المعركة: يوم أن توقفت المدفوعات

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

أنا كنت مسؤولاً عن خدمة “الطلبات” (Orders Service)، وكان زميلي “سعيد” مسؤولاً عن خدمة “المدفوعات” (Payments Service). خدمتي كانت تعتمد بشكل كامل على خدمة سعيد؛ فبعد أن ينشئ المستخدم طلبًا، تقوم خدمتي بمناداة خدمة المدفوعات لتمرير بيانات الدفع. العلاقة بيننا كانت بسيطة ومباشرة… أو هكذا ظننت.

في أحد أيام الخميس، وقبل عطلة نهاية الأسبوع، قام فريق سعيد بنشر تحديث جديد لخدمة المدفوعات. كان تحديثًا بسيطًا، مجرد “refactoring” لبعض أجزاء الكود وتحسين اسم حقل في الـ JSON المُرسل من `amount` إلى `payment_amount` لجعله أوضح. تغيير منطقي، مش هيك؟

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

كانت ورطة حقيقية. خسرنا إيرادات يوم كامل، وفقدنا ثقة بعض العملاء، والأسوأ من ذلك، اهتزت الثقة بين الفرق. يومها، جلست مع نفسي وقلت: “يا أبو عمر، لا بد من وجود طريقة أفضل. لا يمكن أن نستمر في بناء قلعة من الرمال، تنهار مع كل موجة تغيير”. ومن هنا بدأت رحلتي مع ما يُعرف بـ “اختبار العقود” (Contract Testing).

ما هو جحيم التكامل الهش (Fragile Integration Hell)؟

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

الطريقة التقليدية كانت الاعتماد على اختبارات التكامل الشاملة (End-to-End Integration Tests). هذه الاختبارات تقوم بتشغيل النظام بأكمله ومحاكاة سيناريو حقيقي. لكنها تعاني من مشاكل قاتلة:

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

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

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

اختبار العقود هو نهج يهدف إلى ضمان أن خدمتين (مُستهلِك ومُزوِّد) يمكنهما التواصل مع بعضهما البعض بشكل صحيح دون الحاجة إلى إجراء اختبارات تكامل شاملة. الفكرة بسيطة لكنها عبقرية: بدلاً من اختبار التكامل الفعلي بين الخدمات الحية، نختبر كل خدمة بمعزل عن الأخرى، ولكن مع التأكد من أن كلتيهما تلتزمان بـ “عقد” مشترك.

العقد هو مجرد وثيقة (عادةً ملف JSON) تحدد التوقعات. المُستهلِك (Consumer) يقول: “أنا أتوقع أن أرسل لك هذا الطلب، وأنتظر منك هذه الاستجابة بهذا الشكل”. ثم يأخذ المُزوِّد (Provider) هذا العقد ويتحقق من أنه قادر على الوفاء بهذه التوقعات.

كيف يعمل اختبار العقود؟ (Consumer-Driven Contracts)

النهج الأكثر شيوعًا هو “العقود التي يقودها المستهلك” (Consumer-Driven Contracts)، وتعمل بالخطوات التالية:

  1. من جانب المستهلك (Consumer): أثناء اختبار الوحدات (Unit Tests) للكود الذي يقوم بالاتصال بالخدمة الخارجية، يقوم المبرمج بتعريف التوقعات (interactions). على سبيل المثال، “عندما أطلب بيانات المستخدم بالمعرّف 123، أتوقع استجابة بالحالة 200 وجسم JSON يحتوي على `id` و `name`”.
  2. توليد العقد: تقوم أداة اختبار العقود (مثل Pact) بتسجيل هذه التوقعات وتوليد ملف عقد (pact file).
  3. مشاركة العقد: يتم نشر هذا العقد في مكان مركزي يسمى “Pact Broker” (أو يمكن مشاركته بأي طريقة أخرى).
  4. من جانب المزود (Provider): كجزء من عملية الـ CI/CD الخاصة بالمزود، يتم سحب العقود الخاصة بكل المستهلكين الذين يعتمدون عليه.
  5. التحقق من العقد: تقوم أداة اختبار العقود بتشغيل خادم المزود محليًا، ثم تعيد إرسال الطلبات المذكورة في العقد إلى الخادم، وتقارن الاستجابات الفعلية بالاستجابات المتوقعة في العقد.
  6. النتيجة: إذا تطابقت الاستجابات، ينجح الاختبار. إذا كان هناك أي اختلاف (مثل تغيير اسم حقل أو نوع بيانات)، يفشل الاختبار ويتوقف النشر!

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

مثال عملي باستخدام Pact.js

دعونا نأخذ مثالاً بسيطًا. لدينا خدمة أمامية (Frontend App) تطلب بيانات مستخدم من خدمة خلفية (User API).

h3. الخطوة 1: كتابة اختبار المستهلك (Frontend App)

سنستخدم Pact مع Jest في تطبيق JavaScript. في هذا الاختبار، نحدد توقعاتنا من الـ API.


// consumer.test.js
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { fetchUser } from './apiClient'; // هذا هو الكود الذي نريد اختباره

const { like, eachLike } = MatchersV3;

// 1. إعداد بيئة Pact الوهمية
const provider = new PactV3({
  consumer: 'FrontendApp',
  provider: 'UserAPI',
});

describe('API Pact test', () => {
  it('returns a user object', () => {
    // 2. تعريف العقد (التوقعات)
    provider
      .given('a user with ID 1 exists') // الحالة التي يجب أن يكون عليها المزود
      .uponReceiving('a request for a single user')
      .withRequest({
        method: 'GET',
        path: '/users/1',
        headers: { Accept: 'application/json' },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: like({ // 'like' تضمن وجود الحقول بالنوع الصحيح، وليس بالضرورة نفس القيمة
          id: 1,
          name: 'Abu Omar',
          email: 'abu.omar@example.com',
        }),
      });

    // 3. تنفيذ الاختبار
    return provider.executeTest(async (mockServer) => {
      // الكود الفعلي الذي يتصل بالـ API
      const user = await fetchUser(mockServer.url, 1); 

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

عند تشغيل هذا الاختبار، سيقوم Pact بأمرين: أولاً، سيشغل خادمًا وهميًا يستجيب وفقًا للعقد الذي حددناه للتحقق من أن كود `fetchUser` يعمل. ثانيًا، والأهم، سيقوم بإنشاء ملف `FrontendApp-UserAPI.json` (ملف العقد) الذي يصف هذا التفاعل.

h3. الخطوة 2: التحقق من العقد من جانب المزود (User API)

الآن، في مشروع الـ `UserAPI` (لنفترض أنه مكتوب بـ Express.js)، سنكتب اختبارًا للتحقق من العقد.


// provider.test.js
const { Verifier } = require('@pact-foundation/pact');
const path = require('path');
const server = require('./server'); // ملف الخادم الخاص بنا

// 1. تشغيل الخادم قبل الاختبارات
let app;
beforeAll(() => {
    app = server.listen(8081, () => {
        console.log('User API provider listening on port 8081');
    });
});

afterAll((done) => {
    app.close(done);
});

describe('Pact Verification', () => {
  it('validates the expectations of FrontendApp', () => {
    // 2. إعداد أداة التحقق
    const opts = {
      provider: 'UserAPI',
      providerBaseUrl: 'http://localhost:8081', // عنوان الخادم المحلي
      
      // في الواقع، هذا المسار سيشير إلى Pact Broker
      // pactUrls: [process.env.PACT_BROKER_URL]
      // لكن للتجربة، سنستخدم الملف المحلي الذي تم إنشاؤه
      pactUrls: [path.resolve(process.cwd(), '../frontend/pacts/frontendapp-userapi.json')],

      // (اختياري) إعداد حالة المزود
      stateHandlers: {
        'a user with ID 1 exists': () => {
          // هنا نضع الكود الذي يضمن وجود المستخدم 1 في قاعدة البيانات
          // قبل تشغيل الطلب. مثلاً: db.users.insert({ id: 1, ... })
          return Promise.resolve('User 1 created');
        },
      },
    };

    // 3. تشغيل التحقق
    return new Verifier(opts).verifyProvider();
  });
});

عند تشغيل هذا الاختبار في الـ CI pipeline الخاص بـ `UserAPI`، سيقوم Pact بالآتي:

  • يقرأ ملف العقد.
  • يرسل طلب `GET /users/1` إلى الخادم الحقيقي الذي يعمل على `localhost:8081`.
  • يقارن الاستجابة الفعلية من الخادم مع ما هو مسجل في العقد.

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

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

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

الخلاصة: نحو تكامل واثق ومستقل

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

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

أبو عمر

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

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

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

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

آخر المدونات

التكنلوجيا المالية Fintech

حساباتي البنكية كانت جزرًا معزولة: كيف أنقذتني ‘الخدمات المصرفية المفتوحة’ من جحيم إدارة الأموال اليدوية؟

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

3 أبريل، 2026 قراءة المزيد
البنية التحتية وإدارة السيرفرات

طلباتي كانت تختفي بين الخدمات: كيف أنقذني ‘التتبع الموزع’ (Distributed Tracing) من جحيم تحليل الأعطال؟

أشارككم قصة حقيقية عن طلبات كانت تضيع في أنظمتنا المعقدة، وكيف كان التتبع الموزع (Distributed Tracing) هو المنقذ. سنتعمق في هذا المفهوم، من هو ولماذا...

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

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

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

3 أبريل، 2026 قراءة المزيد
أتمتة العمليات

تنبيهاتي كانت تضيع في بحر الإيميلات: كيف أنقذني ChatOps من فوضى إدارة الحوادث والنشر؟

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

3 أبريل، 2026 قراءة المزيد
خوارزميات

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

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

3 أبريل، 2026 قراءة المزيد
صورة المقال
تسويق رقمي

محتواي كان يضيع في الزحام: كيف بنيت آلة لتوليد آلاف الصفحات المستهدفة باستخدام SEO البرمجي (Programmatic SEO)؟

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

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