“يا زلمة، كمان مرة فشل؟” – قصة انتظار لا تنتهي
أذكر ذلك اليوم جيدًا، كان يوم ثلاثاء رماديًا في مكتبي. كنا أنا وفريقي قد أنهينا للتو ميزة جديدة كنا نعمل عليها لأسابيع في خدمة “إدارة المستخدمين”. الكود نظيف، اختبارات الوحدة (Unit Tests) كلها ناجحة، والأمور تبدو “عال العال”. الخطوة التالية؟ دمج الكود وتشغيل اختبارات التكامل الكاملة (End-to-End Integration Tests) على بيئة الاختبار المشتركة (Staging Environment).
ضغطت زر التشغيل بكل ثقة، وذهبت لأعد فنجان قهوة. بعد عشرين دقيقة طويلة، عدت لأجد ما كنت أخشاه: فشل ذريع باللون الأحمر القاني. الرسالة تقول إن خدمة “الفواتير” (Billing Service) لم تُرجع البيانات المتوقعة.
بدأ السيناريو المعتاد الذي حفظته عن ظهر قلب. أرسلت رسالة لفريق الفواتير: “يا جماعة الخير، اختباراتنا تفشل بسببكم، هل غيرتم شيئًا في الـ API؟”. أتى الرد بعد ساعة: “لأ، كل شيء تمام عنا، المشكلة من عندكم”. ثم تبدأ سلسلة من الاجتماعات، ومشاركة سجلات الأخطاء (Logs)، ومحاولة معرفة من المسؤول. في النهاية، بعد نصف يوم ضائع، اكتشفنا أن فريق الفواتير كان قد نشر تغييرًا صغيرًا “غير مؤثر” حسب قولهم، لكنه كسر العقد غير المكتوب بيننا.
تكرر هذا الموقف مرات ومرات. أصبحت إنتاجيتنا مرهونة بالفرق الأخرى. أصبحنا نقضي وقتًا في انتظار إصلاح بيئات الاختبار أكثر من الوقت الذي نقضيه في كتابة الكود الفعلي. شعرت أننا في جحيم لا ينتهي من الاعتمادية والانتظار. وقتها، قلت لنفسي: “لازم يكون في حل أفضل. شغلنا واقف على شلن!”. وهنا بدأت رحلتي مع ما يسمى بـ “اختبار العقود” (Contract Testing).
الجحيم الذي يسمى “اختبارات التكامل الكاملة” (E2E)
قبل أن نغوص في الحل، دعونا نفهم أصل المشكلة. في عالم الخدمات المصغرة (Microservices)، لدينا عشرات أو حتى مئات الخدمات الصغيرة التي تتحدث مع بعضها البعض. اختبارات التكامل الكاملة تحاول محاكاة رحلة مستخدم حقيقية عبر كل هذه الخدمات معًا في بيئة واحدة مشتركة.
نظريًا، هذا يبدو رائعًا. لكن عمليًا، هو كابوس لهذه الأسباب:
- البطء الشديد: تشغيل هذه الاختبارات يستغرق وقتًا طويلًا جدًا، من دقائق إلى ساعات. هذا يقتل حلقة التغذية الراجعة السريعة (Fast Feedback Loop) التي هي أساس التطوير السريع.
- الهشاشة (Flakiness): تفشل هذه الاختبارات لأسباب لا علاقة لها بالكود الذي غيرته. قد تفشل بسبب مشكلة في الشبكة، أو لأن خدمة أخرى معطلة، أو لأن البيانات في قاعدة البيانات المشتركة تغيرت.
- صعوبة تصحيح الأخطاء: عندما يفشل اختبار E2E، يكون من الصعب جدًا تحديد أين تكمن المشكلة بالضبط. هل هي في خدمتي؟ أم في الخدمة الأخرى؟ أم في البيئة نفسها؟
- جحيم الاعتمادية: تتطلب هذه الاختبارات أن تكون جميع الخدمات التي تحتاجها متاحة ومنشورة على بيئة الاختبار بنفس الإصدار المتوافق. هذا يخلق اختناقات (bottlenecks) حيث ينتظر الجميع الجميع.
باختصار، اختبارات التكامل الكاملة في بيئة الخدمات المصغرة المعقدة تصبح عائقًا للإنتاجية بدلاً من كونها شبكة أمان.
طوق النجاة: مقدمة إلى اختبار العقود (Contract Testing)
تخيل لو كان بإمكانك التأكد من أن خدمتك متوافقة مع خدمة أخرى دون الحاجة لتشغيل الخدمة الأخرى على الإطلاق. هذا هو جوهر اختبار العقود.
اختبار العقود هو أسلوب يهدف إلى التحقق من أن خدمتين (مثل خدمة “مستهلكة” للبيانات وخدمة “مزودة” للبيانات) يمكنهما التواصل مع بعضهما البعض بشكل صحيح. يتم ذلك من خلال “عقد” (Contract) يتم الاتفاق عليه بين الطرفين.
العقد هو ببساطة ملف يوثق التوقعات. المستهلك يقول: “أنا أتوقع منك يا مزود أن تعطيني استجابة بهذا الشكل عند استدعاء هذه الواجهة”. والمزود يستخدم هذا العقد ليختبر نفسه ويضمن أنه يفي بهذه التوقعات.
الجمال في هذا الأسلوب هو أن الاختبار يتم بشكل غير متزامن ومستقل. فريق المستهلك يختبر ضد “محاكاة” للمزود، وفريق المزود يختبر ضد “توقعات” المستهلك، وكل ذلك يحدث في بيئة التطوير المحلية لكل فريق وعلى سيرفرات الـ CI/CD الخاصة بهم، بسرعة وبشكل معزول.
كيف يعمل هذا السحر؟
تتم العملية عادةً على ثلاث خطوات رئيسية، وغالبًا ما نستخدم أداة شهيرة مثل Pact لتسهيلها:
- الجانب المستهلك (Consumer): يقوم فريق المستهلك بكتابة اختبار يحدد فيه الطلب (Request) الذي سيرسله إلى المزود، والاستجابة (Response) التي يتوقعها بالضبط. عند تشغيل هذا الاختبار، يتم إنشاء ملف “عقد” (بصيغة JSON عادةً) يحتوي على هذه التفاعلات.
- مشاركة العقد: يتم نشر ملف العقد هذا في مكان مركزي يسمى “وسيط العقود” أو Pact Broker. هذا الوسيط هو بمثابة سجل لجميع العقود بين الخدمات المختلفة.
- الجانب المزود (Provider): يقوم فريق المزود بسحب العقود المتعلقة بخدمتهم من الـ Pact Broker. ثم يقومون بتشغيل اختبار “تحقق” (Verification) يقوم بإعادة إرسال الطلبات الموجودة في العقد إلى خدمتهم الحقيقية، ويقارن الاستجابة الفعلية بالاستجابة المتوقعة في العقد.
إذا نجح المستهلك في إنشاء العقد، ونجح المزود في التحقق منه، فهذا يعطينا ثقة عالية جدًا بأن التكامل بينهما سيعمل بنجاح في البيئة الحقيقية. كل هذا دون الحاجة لبيئة Staging مشتركة ومعقدة!
هيا بنا نُبرمج: مثال عملي باستخدام Pact.js
دعونا نأخذ مثالًا بسيطًا: خدمة `WebApp` (المستهلك) تريد عرض بيانات مستخدم من خدمة `UserAPI` (المزود).
1. جانب المستهلك (WebApp)
في مشروع الـ `WebApp`، سنستخدم Jest و Pact.js لكتابة اختبار يحدد توقعاتنا من `UserAPI`.
// userApiClient.test.js
import { Pact } from '@pact-foundation/pact';
import { getUser } from './userApiClient'; // دالة تستدعي الـ API
// إعداد المزود الوهمي (Mock Provider)
const provider = new Pact({
consumer: 'WebApp',
provider: 'UserAPI',
port: 1234, // منفذ وهمي
});
describe('API Pact test for UserAPI', () => {
beforeAll(() => provider.setup()); // تشغيل السيرفر الوهمي
afterEach(() => provider.verify()); // التحقق من أن كل التفاعلات تم استدعاؤها
afterAll(() => provider.finalize()); // إيقاف السيرفر وإنشاء ملف العقد
describe('getting a user', () => {
it('should return a user object on success', async () => {
// تعريف العقد (التفاعل)
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: 'Abu Omar',
email: 'abu.omar@example.com',
},
},
});
// استدعاء الكود الفعلي الذي يتصل بالـ API
const user = await getUser(provider.mockService.baseUrl, 1);
// التأكد من أن الكود الخاص بنا تعامل مع الاستجابة بشكل صحيح
expect(user.name).toBe('Abu Omar');
});
});
});
عند تشغيل هذا الاختبار، سيقوم Pact بإنشاء ملف `WebApp-UserAPI.json` في مجلد `pacts`. هذا هو العقد!
2. جانب المزود (UserAPI)
الآن، في مشروع `UserAPI` (الذي قد يكون مبنيًا بـ Express.js مثلاً)، سنكتب اختبار تحقق.
// userApi.pact.test.js
import { Verifier } from '@pact-foundation/pact';
import path from 'path';
import server from './server'; // سيرفر Express الخاص بنا
// نبدأ السيرفر قبل الاختبارات
let app;
beforeAll(() => {
app = server.listen(8081, () => {
console.log('UserAPI listening on port 8081');
});
});
afterAll(() => {
app.close();
});
describe('Pact Verification', () => {
it('validates the expectations of WebApp', () => {
const opts = {
provider: 'UserAPI',
providerBaseUrl: 'http://localhost:8081', // عنوان الـ API الحقيقي
pactUrls: [
path.resolve(process.cwd(), '../WebApp/pacts/webapp-userapi.json') // مسار ملف العقد
],
// في حالة استخدام Pact Broker
// pactBrokerUrl: 'https://your-pact-broker.com',
};
return new Verifier(opts).verifyProvider();
});
});
عند تشغيل هذا الاختبار، سيقوم Pact بالتالي:
- يقرأ ملف العقد.
- يرسل طلب `GET /users/1` إلى سيرفر `UserAPI` الحقيقي الذي يعمل على `localhost:8081`.
- يقارن الاستجابة الفعلية من السيرفر مع ما هو موجود في العقد.
إذا تطابقت الاستجابة، ينجح الاختبار. الآن يمكنك أنت وفريق `WebApp` العمل بشكل مستقل مع ثقة كاملة!
نصائح أبو عمر الذهبية
بعد تطبيق هذا الأسلوب في عدة مشاريع، تعلمت بعض الدروس التي أود مشاركتها معكم:
ابدأ صغيرًا ولكن ابدأ الآن
لا تحاول تحويل كل اختبارات التكامل لديك دفعة واحدة. اختر أكثر نقطة تكامل تسبب لك الألم والمشاكل، وابدأ بها. سترى الفائدة بسرعة، وهذا سيشجعك ويشجع الفرق الأخرى على تبني هذا النهج.
الـ Pact Broker هو صديقك الصدوق
صحيح أنه يمكنك تبادل ملفات العقد يدويًا، لكن القوة الحقيقية تكمن في استخدام وسيط مثل Pact Broker. فهو لا يخزن العقود فقط، بل يوفر واجهة رسومية رائعة، ويقوم بإدارة الإصدارات، والأهم من ذلك، يخبرك ما هي إصدارات الخدمات التي يمكن نشرها معًا بأمان (can-i-deploy). هذا هو مفتاح تحقيق CI/CD سلس وسريع.
ليس حلًا سحريًا، بل أداة قوية
اختبار العقود لا يغني عن اختبارات الوحدة (Unit Tests)، بل يكملها. قد لا يغني تمامًا عن وجود عدد قليل جدًا من اختبارات E2E الأساسية (Smoke Tests) للتأكد من أن كل شيء يعمل معًا في البيئة الحقيقية. الهدف هو نقل الغالبية العظمى من اختبارات التكامل من قمة هرم الاختبار البطيئة إلى طبقة وسطى أسرع وأكثر موثوقية.
العقد هو الحكم: التواصل أولًا
اختبار العقود ليس مجرد أداة تقنية، بل هو أداة تواصل. إنه يجبر فرق المستهلك والمزود على التحدث معًا والاتفاق على شكل الـ API. العقد يصبح هو “مصدر الحقيقة الوحيد” (Single Source of Truth) للتفاعل بين الخدمتين. “بطل في مجال للمجاملات، العقد هو الحكم”.
الخلاصة: استعد سرعتك وثقتك
كانت اختبارات التكامل الكاملة تسرق منا أغلى ما نملك كمطورين: الوقت والتركيز. حولت عملية التطوير إلى سلسلة من الانتظار واللوم المتبادل. جاء اختبار العقود كطوق نجاة، حيث أعاد لنا السرعة والثقة في تغيير ونشر خدماتنا بشكل مستقل. لقد سمح لنا بالفشل بسرعة (Fail Fast) وتصحيح الأخطاء في بيئة التطوير المحلية، بدلاً من اكتشافها بعد ساعات في بيئة مشتركة معقدة.
نصيحتي الأخيرة لك: إذا كنت تعاني من نفس الألم في بيئة الخدمات المصغرة، فلا تتردد. بدل ما تضل تستنى، ابدأ ابحث في اختبار العقود واكتب أول عقد لك. صدقني، إنتاجيتك وصحتك النفسية راح يشكروك. 🚀