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

يا جماعة الخير، مساكم الله بالخير. اسمحوا لي اليوم أحكي لكم قصة صارت معي ومع فريقي قبل كم سنة، قصة علّمتنا درس قاسي لكنه ثمين جداً. كانت ليلة خميس، والكل متحمس يبدأ عطلة نهاية الأسبوع. كان عنا تحديث “بسيط” على خدمة المستخدمين (User Service)، مجرد تعديل صغير في حقل من الحقول اللي بترجعها الـ API.

فريق خدمة المستخدمين عمل شغله، الاختبارات الوحدوية (Unit Tests) كلها نجحت، كل شيء أخضر عندهم. ضغطوا على زر النشر (Deploy) وهم مرتاحين البال. في نفس الوقت، فريقنا، اللي كان مسؤول عن خدمة الطلبات (Order Service)، كان برضه مرتاح البال، اختباراتنا كلها تمام والخدمة شغالة زي الساعة.

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

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

ما هو جحيم فشل التكامل الصامت؟

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

الاختبارات التقليدية لا تكفي دائمًا:

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

هنا يكمن “الفشل الصامت”. كل شيء يبدو على ما يرام في عزلة، لكن عند التكامل في بيئة الإنتاج، تحدث الكارثة.

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

تخيل أن العلاقة بين خدمتين هي عقد قانوني. خدمة “المستهلك” (Consumer) تقول: “أنا أتوقع منك يا خدمة ‘المزود’ (Provider) أن تعطيني معلومة بهذا الشكل المحدد”. خدمة المزود تلتزم بهذا العقد. اختبار العقود هو ببساطة عملية التحقق من أن كلا الطرفين لا يزالان ملتزمين بهذا العقد.

إنه لا يختبر *منطق* الخدمة، بل يختبر *شكل* الحوار بينهما. هل ما زالت نقاط النهاية (Endpoints) كما هي؟ هل ما زالت أسماء الحقول كما هي؟ هل أنواع البيانات (Data Types) لم تتغير؟

اللاعبون الأساسيون في اللعبة

  • المستهلك (Consumer): الخدمة التي تطلب البيانات (في قصتنا، كانت خدمة الطلبات Order Service).
  • المزوّد (Provider): الخدمة التي توفر البيانات (خدمة المستخدمين User Service).
  • العقد (Contract): ملف يصف التوقعات التي لدى المستهلك من المزوّد. يتم إنشاؤه من طرف المستهلك.

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

كيف يعمل اختبار العقود عمليًا؟ (مع أمثلة Pact)

أشهر أداة في هذا المجال هي Pact. دعنا نرى كيف كان بإمكان Pact أن ينقذنا في تلك الليلة المشؤومة.

الخطوة 1: جانب المستهلك (Consumer-Side)

في خدمة الطلبات (Order Service)، التي تستخدم Node.js على سبيل المثال، سنكتب اختبارًا باستخدام Jest و Pact. هذا الاختبار يفعل شيئين:

  1. يُشغل خادمًا وهميًا (Mock Server) يتظاهر بأنه خدمة المستخدمين.
  2. يُعرّف “التفاعل” المتوقع مع هذا الخادم الوهمي.
  3. يتحقق من أن كود المستهلك قادر على التعامل مع الاستجابة المتوقعة.

إذا نجح كل هذا، يقوم Pact بإنشاء ملف العقد (JSON file).


// consumer-test.spec.js (في خدمة الطلبات)
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { fetchUser } from './apiClient'; // الكود الفعلي الذي يستدعي الـ API

// 1. إعداد بيئة Pact
const provider = new PactV3({
  consumer: 'OrderService', // اسم المستهلك
  provider: 'UserService', // اسم المزوّد
});

const { like, string } = MatchersV3;

// تعريف شكل المستخدم المتوقع
const USER_EXPECTATION = {
  // هنا كانت المشكلة! كنا نتوقع userId
  // لو كتبنا العقد هكذا من البداية، لكنا اكتشفنا الخطأ
  userId: like('c212a2c6-add4-4bb3-8b4c-972134e5e57a'), 
  name: string('أبو عمر'),
  email: string('abu.omar@example.com'),
};

describe('API Client for UserService', () => {
  it('returns a user object', () => {
    // 2. تعريف التفاعل (العقد)
    provider
      .uponReceiving('a request to get a user by ID')
      .withRequest({
        method: 'GET',
        path: '/users/c212a2c6-add4-4bb3-8b4c-972134e5e57a',
        headers: { Accept: 'application/json' },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: USER_EXPECTATION, // استخدم الشكل المتوقع هنا
      });

    // 3. تنفيذ الاختبار
    return provider.executeTest(async (mockServer) => {
      // الكود الخاص بنا يستدعي الخادم الوهمي الآن
      const user = await fetchUser(mockServer.url, 'c212a2c6-add4-4bb3-8b4c-972134e5e57a');
      
      // نتأكد أن الكود الخاص بنا تعامل مع الاستجابة بشكل صحيح
      expect(user.userId).toBe('c212a2c6-add4-4bb3-8b4c-972134e5e57a');
    });
  });
});

بعد تشغيل هذا الاختبار بنجاح، سينتج Pact ملفًا اسمه OrderService-UserService.json. هذا هو عقدنا الرسمي.

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

هذا هو “المكتب” الذي نحفظ فيه كل عقودنا. الـ Pact Broker هو تطبيق مستقل يعمل كمركز لتبادل العقود ونتائج التحقق منها. بدلاً من تبادل ملفات JSON يدويًا، يقوم الـ CI/CD الخاص بالمستهلك برفع العقد تلقائيًا إلى الـ Broker.

الخطوة 3: جانب المزوّد (Provider-Side)

الآن ننتقل إلى خدمة المستخدمين (UserService)، التي ربما تكون مكتوبة بلغة Java و Spring Boot. هنا، سنكتب اختبارًا يقوم بالآتي:

  1. يسحب أحدث نسخة من العقد من الـ Pact Broker.
  2. يُشغل خدمة المستخدمين الحقيقية على منفذ محلي.
  3. Pact يقوم “بإعادة تشغيل” الطلبات المذكورة في العقد ضد الخدمة الحقيقية.
  4. يقارن الاستجابات الفعلية من الخدمة مع الاستجابات المتوقعة في العقد.

إذا كان هناك أي اختلاف (مثل تغيير userId إلى user_id)، سيفشل الاختبار، وبالتالي ستفشل عملية البناء (Build) في الـ CI/CD، ولن يتم نشر التغيير الكارثي أبدًا!


// ProviderVerificationTest.java (في خدمة المستخدمين)
@RunWith(PactVerificationSpringRunner.class)
@PactBroker // اسحب العقود من الـ Broker
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserServicePactVerificationTest {

    // Pact سيقوم بتشغيل الخدمة الحقيقية لاختبارها
    @TestTarget
    public final Target target = new SpringBootHttpTarget();

    // نحتاج أحيانًا لتهيئة بعض الحالات قبل الاختبار
    // مثلاً، نتأكد من وجود مستخدم بالـ ID المطلوب في العقد
    @State("a user with ID c212a2c6-add4-4bb3-8b4c-972134e5e57a exists")
    public void userExistsState() {
        // هنا تضع الكود الذي يجهز الحالة
        // مثلاً، إضافة مستخدم وهمي إلى قاعدة البيانات المؤقتة للاختبار
        userRepository.save(new User("c212a2c6-add4-4bb3-8b4c-972134e5e57a", "أبو عمر", "abu.omar@example.com"));
    }
}

عندما يقوم فريق خدمة المستخدمين بتغيير الحقل إلى user_id، سيقومون بتشغيل هذا الاختبار. سيقوم Pact بإرسال طلب GET /users/... إلى خدمتهم. ستستجيب الخدمة بـ JSON يحتوي على {"user_id": "..."}. سيقارن Pact هذه الاستجابة مع العقد الذي يتوقع {"userId": "..."}. سيجد الاختلاف، وسيفشل الاختبار على الفور! سيحصل المطور على رسالة واضحة: “لقد كسرت العقد مع OrderService. الحقل ‘userId’ مفقود.”

وهكذا، يتم اكتشاف المشكلة في مرحلة التطوير، وليس في منتصف ليلة خميس.

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

يا جماعة، تطبيق هذا المفهوم يحتاج لبعض الجهد في البداية، لكنه يوفر أضعاف هذا الجهد لاحقًا. وهذه بعض النصائح من القلب:

  • ابدأ صغيرًا (Start Small): لا تحاول تطبيق هذا على كل خدماتك مرة واحدة. اختر خدمتين حيويتين تتواصلان بكثرة وابدأ بهما. أثبت نجاح الفكرة ثم توسع.
  • العقد ليس وثيقة API كاملة: هذه نقطة مهمة جدًا. يجب على المستهلك أن يعرّف فقط ما يستخدمه بالفعل. إذا كانت استجابة الـ API تحتوي على 20 حقلًا والمستهلك يستخدم 3 فقط، فيجب أن يحتوي العقد على هذه الثلاثة فقط. هذا يعطي المزوّد حرية تغيير الـ 17 حقلاً الآخرين دون كسر العقد.
  • اجعلها جزءًا من الـ CI/CD: القوة الحقيقية لاختبار العقود تظهر عندما تكون مؤتمتة بالكامل. يجب أن تفشل عملية البناء (Build) للمزوّد إذا كسر عقدًا ما. ويجب أن تمنع عملية النشر (Deploy) إذا كان هناك عقد غير متحقق منه.
  • فكر بطريقة “المستهلك أولاً”: هذا المنهج يسمى “Consumer-Driven Contract Testing” لسبب. إنه يجبر المطورين على التفكير في مستخدمي الـ API الخاصة بهم أولاً. قبل إجراء أي تغيير، السؤال الأول يجب أن يكون: “من سيتأثر بهذا التغيير؟”.

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

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

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

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

أبو عمر

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

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

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

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

آخر المدونات

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

كان تتبع الطلبات كابوساً: كيف أنقذتنا ‘الشبكة الخدمية’ (Service Mesh) من جحيم العمى التشغيلي؟

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

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

كانت المدخلات غير المتوقعة تحطم تطبيقنا: كيف أنقذتنا ‘البرمجة الدفاعية’ من جحيم الانهيارات المفاجئة؟

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

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

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

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

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

كان التحقق من وجود البيانات يقتل قاعدة بياناتنا: كيف أنقذنا ‘فلتر بلوم’ (Bloom Filter) من جحيم الاستعلامات غير الضرورية؟

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

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