يا جماعة الخير، السلام عليكم ورحمة الله.
بتذكر مرة، قبل كم سنة، كنا شغالين على إطلاق ميزة جديدة ومهمة جدًا في واحد من المشاريع الكبيرة. كانت الساعة حوالي 2 بعد نص الليل، والقهوة صارت زي المي، والعيون حمرا من كتر التحديق في الشاشات. الميزة كانت بتعتمد على تكامل بين فريقين: فريقنا (الواجهة الأمامية – Frontend) وفريق الواجهة الخلفية (Backend).
وفجأة… كل شي انهار. الميزة اللي كانت شغالة تمام على بيئة التطوير المحلية لكل واحد فينا، بطلت تشتغل على بيئة الاختبار المشتركة (Staging). طبعًا، بلّش الكابوس اللي كل مطور بعرفه منيح: لعبة “مين السبب؟”.
فريقنا يفتح تذكرة (Ticket): “الـ API بترجع بيانات غلط!”. فريق الـ Backend يرد بكل ثقة: “مستحيل، عندي كله شغال تمام، المشكلة من عندكم، أكيد بتبعتوا الطلب غلط”. واحنا نرجع نرد: “هي الطلب قدامك مصوّر، وهي الرد اللي بيجينا، كيف المشكلة من عنا؟!”. وظلّينا في هالحلقة المفرغة لساعات، اجتماعات طارئة ورسائل على Slack ما بتخلص، والكل متوتر والكل برمي اللوم عالتاني. شعار المرحلة كان: “مش من عندي المشكلة!”.
في النهاية، بعد تضييع ساعات ثمينة، اكتشفنا إنه واحد من الزملاء في فريق الـ Backend غيّر اسم حقل واحد في الـ JSON اللي بيرجع من الـ API. تغيير بسيط جدًا، من userId إلى user_id. تغيير بريء، لكنه كان كفيل يهدّم كل الشغل. هاي الليلة كانت من الليالي اللي بتخليك تفكر: “أكيد في طريقة أحسن من هيك!”.
وهون يا جماعة، كانت بداية رحلتي مع مفهوم غيّر طريقة تفكيرنا في التكامل بين الخدمات تمامًا: الاختبار التعاقدي (Contract Testing). خليني أحكيلكم كيف هالأداة البسيطة في فكرتها، أنقذتني وأنقذت فريقي من جحيم الاتهامات المتبادلة.
ما هو الاختبار التعاقدي (Contract Testing)؟ وليش هو مهم؟
ببساطة شديدة، الاختبار التعاقدي هو طريقة للتأكد من أن خدمتين (زي تطبيق ويب وواجهة برمجية خلفية API) “متفقين” على كيفية التواصل بينهما، ورح يضلوا متفقين حتى لو كل واحد فيهم بتطور بشكل مستقل.
فكر فيه زي عقد حقيقي بين طرفين:
- المستهلك (Consumer): هو الطرف اللي بيطلب الخدمة (مثلاً، تطبيق الموبايل أو الواجهة الأمامية).
- المزوّد (Provider): هو الطرف اللي بيقدم الخدمة (مثلاً، الـ Backend API).
العقد هاد بحدد بالضبط شو “المستهلك” بتوقع من “المزوّد”. مثلاً، المستهلك بقول: “أنا لما أبعتلك طلب GET على مسار /users/1، بتوقع منك ترد عليّ بـ status 200 وترجعلي بيانات بصيغة JSON فيها حقل اسمه name من نوع String وحقل اسمه age من نوع Number”.
الاختبار التعاقدي بيتحقق من شغلتين رئيسيتين:
- هل توقعات “المستهلك” منطقية؟ (يعني هل هو فعلاً بيستخدم كل البيانات اللي بيطلبها؟)
- هل “المزوّد” بيلتزم بهاي التوقعات (العقد)؟
الجميل في الموضوع إنه بيفصل الاختبارات. كل فريق بيختبر الجزء الخاص فيه بشكل معزول، بدون الحاجة لتشغيل النظام كاملًا. وهذا هو الفرق الجوهري بينه وبين اختبارات التكامل (Integration Tests) التقليدية اللي بتكون بطيئة، معقدة، وبتحتاج بيئة كاملة وشغالة عشان تتم.
كيف يعمل الاختبار التعاقدي؟ خلينا نفصّلها خطوة بخطوة
عشان الصورة توضح أكثر، خلينا نستخدم أشهر أداة في هذا المجال وهي Pact. العملية بتمر بثلاث مراحل رئيسية:
الخطوة الأولى: المستهلك (Consumer) يحدد توقعاته
كل شي بيبدأ من عند المستهلك. فريق الواجهة الأمامية (أو أي خدمة مستهلكة أخرى) بيكتب اختبار يحدد فيه توقعاته من الـ API. هذا الاختبار ما بيتصل بالـ API الحقيقية، بل بيتصل بـ Mock Server (خادم وهمي) بتوفره مكتبة Pact.
لنفترض عنا تطبيق React (المستهلك) بده يجيب بيانات مستخدم من الـ API (المزوّد). ممكن نكتب اختبار باستخدام Jest و Pact.js كالتالي:
// consumer-test.js (مثال في JavaScript باستخدام Pact.js)
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike } = MatchersV3;
// 1. إعداد بيئة الاختبار الخاصة بـ Pact
const provider = new PactV3({
consumer: 'MyWebApp', // اسم المستهلك
provider: 'UserAPI', // اسم المزوّد
});
// 2. وصف "التفاعل" أو "الطلب" المتوقع
describe('Fetching user data from UserAPI', () => {
it('returns a user object for a given ID', () => {
// تحديد شكل الطلب اللي رح يبعته المستهلك
provider.given('a user with ID 1 exists')
.uponReceiving('a request to get user 1')
.withRequest({
method: 'GET',
path: '/users/1',
headers: { Accept: 'application/json' },
})
// تحديد شكل الرد اللي بيتوقعه المستهلك
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: like(1), // بتوقع يكون رقم، القيمة 1 مجرد مثال
name: like('Abu Omar'), // بتوقع يكون نص
email: like('abu.omar@example.com'), // بتوقع يكون نص
},
});
return provider.executeTest(async (mockServer) => {
// 3. تشغيل الكود الحقيقي تبعنا اللي بيعمل الطلب
// لكن بنخليه يطلب من الـ Mock Server اللي عمله Pact
const response = await fetch(`${mockServer.url}/users/1`, {
headers: { Accept: 'application/json' },
});
const user = await response.json();
// تأكيد أن الكود تبعنا بيتعامل مع الرد بشكل صحيح
expect(user.id).toBe(1);
expect(user.name).toBe('Abu Omar');
});
});
});
لما نشغل هذا الاختبار، Pact بيعمل شغلتين:
- بيتحقق إنه الكود تبعنا (في
executeTest) فعلاً بيعمل طلب متوافق مع اللي وصفناه. - الأهم: بينتج ملف JSON خاص اسمه ملف العقد (Pact File). هذا الملف هو التوثيق الدقيق للاتفاق بين الطرفين.
الخطوة الثانية: مشاركة العقد (The Pact File)
ملف العقد هذا هو “رسول السلام” بين الفريقين. لازم يوصل لفريق المزوّد (الـ Backend). ممكن تبعته بالإيميل أو تحطه في مستودع مشترك، لكن الطريقة الاحترافية هي استخدام أداة اسمها Pact Broker.
الـ Pact Broker هو سيرفر خاص بتخزن عليه كل العقود بين كل الخدمات في نظامك. بيعمل أرشفة، وبعطيك واجهة رسومية تشوف فيها مين متوافق مع مين، ومين رح “يكسر” مين لو عمل تغيير معين. بنصح فيه بشدة.
الخطوة الثالثة: المزوّد (Provider) يتحقق من التزامه بالعقد
الآن، الكرة في ملعب فريق المزوّد. فريق الـ Backend بياخد ملف العقد اللي تولّد من المستهلك، وبيشغل اختبار تحقق (Verification Test) على الـ API تبعتهم.
هذا الاختبار بيعمل الآتي:
- Pact بيقرأ كل طلب موصوف في ملف العقد.
- بيعمل طلب حقيقي على الـ API تبعت المزوّد (اللي بتكون شغالة محليًا).
- بيقارن الرد الفعلي اللي رجع من الـ API مع الرد المتوقع والموصوف في العقد.
إذا تطابق الرد الفعلي مع الرد المتوقع في العقد، الاختبار بينجح. مبروك، المزوّد ملتزم بالعقد! ✅
إذا كان في أي اختلاف (اسم حقل تغير، نوع بيانات اختلف، status code غلط…)، الاختبار بيفشل فورًا! ❌
هيك بكون شكل الاختبار من جهة المزوّد (مثال بـ Node.js/Express):
// provider-verification.js (مثال في JavaScript باستخدام Pact.js)
const { Verifier } = require('@pact-foundation/pact');
const server = require('./my-api-server'); // ملف السيرفر الحقيقي تبعنا
// ... (كود تشغيل السيرفر على بورت معين)
describe('Pact Verification', () => {
it('validates the expectations of MyWebApp', () => {
const opts = {
provider: 'UserAPI', // اسم المزوّد لازم يطابق اللي في العقد
providerBaseUrl: 'http://localhost:8080', // عنوان الـ API وهي شغالة محليًا
// جلب العقود من الـ Pact Broker أو من ملف محلي
pactBrokerUrl: 'https://your-pact-broker.com',
// أو pactUrls: ['./path/to/pactfile.json']
// ... إعدادات أخرى
};
// Pact رح يتكفل بالباقي
return new Verifier(opts).verifyProvider();
});
});
سيناريو عملي: لما المزوّد يقرر يغيّر إشي “بسيط”
نرجع لقصتنا الأصلية. لنفترض أن المطور في فريق الـ Backend قرر يتبع معايير جديدة ويغير اسم الحقل من email إلى email_address.
بدون اختبار تعاقدي:
- المطور بيعمل التغيير وبيدفعه للمستودع (git push).
- الـ CI/CD pipeline بتشتغل، كل اختبارات الوحدة (Unit Tests) بتنجح.
- الكود بيوصل لبيئة الاختبار (Staging).
- فريق الواجهة الأمامية بيلاحظوا إنه صفحة المستخدم انهارت.
- تبدأ حفلة “مش من عندي المشكلة!”.
مع وجود اختبار تعاقدي:
- المطور بيعمل التغيير على الكود المحلي.
- قبل ما يعمل push، بيشغل اختبار التحقق من العقد (Provider Verification Test).
- الاختبار بيفشل فورًا! 💥 بيطلعله رسالة واضحة جدًا:
“Failure: body does not match. Expected ’email’ but was missing.”
- المطور بيعرف فورًا إنه هذا التغيير “البسيط” هو في الحقيقة “Breaking Change” رح يأثر على المستهلك (MyWebApp).
- الآن عنده خيارين: إما يتراجع عن التغيير، أو يروح يتكلم مع فريق الواجهة الأمامية ويقولهم: “يا جماعة، أنا محتاج أغير اسم الحقل، لازم تحدثوا توقعاتكم في العقد أولاً”.
لاحظتوا الفرق؟ المشكلة انكشفت في أقل من دقيقة، على جهاز المطور المحلي، بدون ما تضيع ثانية واحدة من وقت أي فريق آخر. لا اجتماعات طارئة، لا اتهامات، ولا وجعة راس.
نصائح من خبرة أبو عمر
-
ابدأ صغيرًا: لا تحاول تطبق الاختبار التعاقدي على كل خدماتك مرة واحدة. اختار نقطة تكامل واحدة حساسة بين فريقين بتسبب مشاكل دايمًا، وابدأ منها. النجاح في نقطة واحدة رح يشجع باقي الفرق.
-
اجعلها جزءًا من الـ CI/CD: القوة الحقيقية للاختبار التعاقدي بتظهر لما يصير آلي. أي Pull Request لازم يشغل اختبارات العقد الخاصة فيه. فشل اختبار العقد لازم يمنع دمج الكود (merge).
-
استخدم Pact Broker (أو بديل): إدارة ملفات العقود يدويًا متعبة جدًا. استثمر وقت في إعداد Broker، لأنه بيسهل كل العملية وبعطيك رؤية شاملة وقوية.
-
الاختبار التعاقدي لا يغني عن كل الاختبارات: هذا الاختبار لا يختبر منطق العمل (Business Logic) داخل المزوّد. هو فقط يضمن أن “شكل” التواصل صحيح. لازم تظل تكتب Unit Tests ممتازة وبعض اختبارات الـ End-to-End للحالات الحرجة.
-
التواصل يبقى الملك: هذه الأداة بتنظم التواصل، ما بتلغيه. لما يفشل اختبار عقد، هاي مش مشكلة تقنية وبس، هاي دعوة لفنجان قهوة (أو مكالمة Zoom) بين الفريقين عشان يتفقوا على العقد الجديد. “يا جماعة، العقد انكسر، خلينا نحكي.”
الخلاصة: وداعًا لـ “مش من عندي!” 👋
في عالم الخدمات المصغرة (Microservices) حيث تتطور الفرق وتطلق تحديثاتها بشكل مستقل، لم يعد “الأمل” استراتيجية جيدة لضمان التكامل. الاختبار التعاقدي يقدم حلاً هندسيًا وعمليًا لهذه الفوضى.
إنه يغير ديناميكية العمل بين الفرق من الشك والاتهام إلى الثقة والتعاون المبني على “عقد” واضح ومؤتمت. هو بمثابة المصافحة التقنية التي تضمن أن الجميع على نفس الصفحة، وتسمح لكل فريق بالإبداع والسرعة في مجاله وهو واثق من أن تكامله مع الآخرين محمي ومضمون.
الاستثمار في تعلم وتطبيق الاختبار التعاقدي هو استثمار في جودة برمجياتك، وفي سرعة التطوير، والأهم من ذلك كله، في صحة فريقك النفسية وسلامة علاقاتهم العملية. صدقوني، راحة البال اللي بتيجي معاه لا تقدر بثمن. 🙏