كيف تختبر خدماتك المصغرة (Microservices) لضمان جاهزيتها للإنتاج؟
تصف بنية الخدمات المصغرة (Microservices) منهجية تقسيم التطبيق إلى مجموعة من المكونات الأصغر حجماً والموجهة نحو حل مشكلات محددة. تتواصل هذه المكونات فيما بينها عبر بروتوكولات شائعة مثل HTTP أو بروتوكول TCP الأخف وزناً. قد تتساءل: هل الاختبارات مهمة بالنسبة لي؟ باختصار: نعم، إنها ضرورية للغاية.
يُعد اختبار البرمجيات أمراً حيوياً لعدة أسباب، أهمها:
- توفير كبير في التكاليف والوقت.
- تعزيز الأمان والحماية.
- تحسين جودة المنتج (تقليل الأخطاء والعيوب).
- زيادة رضا العملاء.
- راحة البال للمطورين والفرق.
لا أحد يحب تطبيقاً مليئاً بالأخطاء ويتوقف عن العمل بلا سبب وجيه. ولا داعي للحديث عن مخاطر ضعف الأمان الذي يسمح للمخترقين بسرقة بيانات الاعتماد وحتى الأموال. طالما أنك تطور تطبيقاً سيستخدمه المستخدمون ويتسم ببعض التعقيد، فإن الاختبارات لا ينبغي أن تكون خياراً، بل يجب أن تكون إلزامية.
ما هي أنواع الاختبارات التي يجب أن أكتبها؟
هناك أنواع مختلفة من اختبارات البرمجيات. تشمل أنواع الاختبارات الوظيفية (Functional Testing):
- اختبار الوحدة (
Unit Testing) - اختبار التكامل (
Integration Testing) - اختبار الدخان (
Smoke Testing) - اختبار الانحدار (
Regression Testing) - اختبار السلامة (
Sanity Testing) - اختبار القبول / التجريبي (
Beta/Acceptance Testing) - اختبار شامل من البداية إلى النهاية (
End to End (e2e) Testing)
أما أنواع الاختبارات غير الوظيفية (Non-functional Testing) فتشمل:
- اختبار الأداء (
Performance Testing) - اختبار التحميل (
Load Testing) - اختبار الإجهاد (
Stress Testing) - اختبار الأمان (
Security Testing) - اختبار الامتثال (
Compliance Testing) - اختبار قابلية الاستخدام (
Usability Testing)
كلما زاد تعقيد التطبيق، زاد عدد أنواع الاختبارات التي ستستخدمها. الاختبارات الأساسية التي يجب عليك استخدامها دائماً هي:
- اختبار الوحدة (
Unit Testing) - اختبار التكامل (
Integration Testing) - اختبار شامل من البداية إلى النهاية (
E2E Testing) بالاشتراك مع اختبار الانحدار (Regression Testing) واختبار الأمان (Security Testing)
تتم العملية على النحو التالي: أولاً، تكتب اختبارات للتحقق مما إذا كان تطبيقك يتصرف كما هو متوقع في جميع الجوانب تقريباً، بما في ذلك الحالات الهامشية (corner cases). ثانياً، إذا كان تطبيقك قيد التشغيل بالفعل، فإنك تكتب اختبارات للتحقق مما إذا كانت أي تغييرات جديدة على الكود تكسر الوظائف الحالية.
ملاحظة جانبية: بالإضافة إلى هذه الاختبارات الأساسية التي يجب استخدامها في أي نوع من البرامج، هناك اختبارات إضافية يجب عليك كتابتها للخدمات المصغرة. لا تنسَ اختبارات التحميل (load tests)، على سبيل المثال، للتحقق من سلوك نظامك تحت ظروف التحميل العادية والمتوقعة.
تطبيق عملي: اختبار خدمة مصغرة باستخدام Node.js و NestJS
في الأمثلة التالية، سنرى كيف يمكننا تطبيق أنواع اختبارات البرمجيات الأساسية المذكورة أعلاه في خدمة مصغرة. تستخدم هذه الخدمة المصغرة بروتوكول TCP للتواصل ومكتوبة بلغة Node.js باستخدام إطار عمل Nest Framework. إذا كان NestJS يبدو جديداً بالنسبة لك، فلا تقلق – كل ما تحتاج لمعرفته هو ما يلي:
“
Nestهو إطار عمل لبناء تطبيقات خادمNode.jsفعالة وقابلة للتوسع. يستخدمJavaScriptالحديث، مبني باستخدامTypeScript(ويحافظ على التوافق معJavaScriptالنقي) ويجمع عناصر من البرمجة الكائنية التوجه (OOP - Object Oriented Programming)، والبرمجة الوظيفية (FP - Functional Programming)، والبرمجة التفاعلية الوظيفية (FRP - Functional Reactive Programming). تحت الغطاء، يستخدمNestإطار عملExpress، ولكنه يوفر أيضاً التوافق مع مجموعة واسعة من المكتبات الأخرى، مثلFastify، مما يتيح سهولة استخدام عدد لا يحصى من المكونات الإضافية (third-party plugins) المتاحة.” – وصف مستودعGithubالرسمي.
لهذا المثال، سنستخدم وحدة بسيطة، اسمها: user، مع دالة بسيطة createUser، ستقوم بإنشاء مستخدم جديد في قاعدة بياناتنا. يبدو هيكل المجلدات للوحدة كما يلي:
لدينا متحكم (controller) يستمع لرسالة create_user. بعد إجراء التحقق باستخدام ValidationPipe، سيستدعي دالة بنفس الاسم داخل خدمته (service).


داخل الخدمة، نقوم بتجزئة كلمة مرور المستخدم (hash the user password). ثم باستخدام TypeORM، نقوم بحفظ مستخدم جديد داخل قاعدة بياناتنا.

لهذه الوحدة، نستخدم TypeORM كـ ORM المرتبط بجدول User، ووحدة أخرى تسمى UtilsModule والتي تحتوي على بعض الدوال المساعدة (helper functions):


اختبار الوحدة (Unit Testing)
الوحدة (unit) هي أصغر جزء قابل للاختبار في التطبيق، مثل الدوال (functions) أو الفئات (classes) أو الإجراءات (procedures). اختبار الوحدة هو طريقة لاختبار البرمجيات يتم من خلالها اختبار الوحدات الفردية من الشيفرة المصدرية لتحديد ما إذا كانت تعمل بشكل صحيح. بشكل أساسي، تُكتب اختبارات الوحدة للتأكد من أن كل تطبيق بسيط لأشكال مختلفة من الشيفرة (الدوال، الفئات، وما إلى ذلك) يفي بتصميمه ومتطلباته ويتصرف كما هو متوقع. الهدف من اختبار الوحدة هو فصل كل جزء من البرنامج واختبار أن الأجزاء الفردية تعمل بشكل صحيح. هذا يعني أنه سيتم استبدال الأجزاء الأخرى من الشيفرة التي ليست جزءاً مباشراً من الوحدة التي يتم اختبارها (ولكنها مرتبطة بها) بنماذج وهمية (mocked).
في حالتنا، الدالة التي نريد اختبارها (createUser) هي الوحدة التي نريد اختبارها. هذا يعني أنه يتعين علينا عزلها عن المكونات الأخرى. لذلك، يجب علينا إنشاء نموذج وهمي لفئة مستودع المستخدم (user repository class) التي تمثل الارتباط بقاعدة البيانات باستخدام TypeORM. إذا قمنا بتحليل الدالة (تلك الموجودة في الخدمة)، نرى أن كل ما تفعله هو تجزئة كلمة مرور ثم حفظ كائن User داخل قاعدة بياناتنا. بناءً على هذه الحقيقة، نكتب مجموعة الاختبار التالية:

أولاً، نكتب دالة beforeAll التي تنشئ وحدة الاختبار الخاصة بنا. ثم نستبدل المستودع الأصلي بفئتنا الوهمية (mock class)، والتي ستعيد فقط الكائن الذي نريد حفظه في قاعدة البيانات. في دالتنا، كان لدينا متطلب واحد مع حالة هامشية واحدة: إنشاء كائن مستخدم جديد بخصائص معينة (email، password)، ولكن بكلمة مرور مجزأة (hashed password).
لقد قمنا بإنشاء نموذج وهمي لدالة save()، لأنها من TypeORM، خارج وحدتنا، وقمنا بتجاوزها بدالة بسيطة تعيد الكائن الذي مررناه. لذلك، كل ما كان علينا فعله هو التحقق مما إذا كنا نرسل الكائن بخاصية البريد الإلكتروني (email property) الصحيحة وبالتجزئة الصحيحة (correct hash).
اختبار التكامل (Integration Testing)
اختبار التكامل هو طريقة لاختبار البرمجيات يتم من خلالها اختبار وحدات الشيفرة المصدرية للتحقق من الوظائف المجمعة. تُكتب اختبارات الوحدة بشكل أساسي للتأكد من أن الشيفرة تفي بتصميمها ومتطلباتها وتتصرف كما هو متوقع. الهدف من اختبار التكامل هو دمج الوحدات المختلفة واختبار ما إذا كانت تتفاعل بشكل صحيح.
الآن، في مثالنا، نجمع وحدة UserModule مع وحدة TypeORM (الاعتمادية) للتحقق مما إذا تم حفظ المستخدم في قاعدة البيانات. مرة أخرى، لدينا نفس الدالة من الأعلى، ولكن هذه المرة مع الاختبار التالي:

هذه المرة، في دالة beforeAll، لا نقوم بإنشاء نموذج وهمي (mock) لـ userRepository. بدلاً من ذلك، نستخدم المستودع الأصلي، بالإضافة إلى أننا نضيف databaseModule الذي ينشئ الاتصال بقاعدة بياناتنا. في الوقت نفسه، لأننا نستخدم قاعدة بيانات حقيقية الآن، يجب علينا كتابة عدة دوال ستقوم بإعداد قاعدة بياناتنا للاختبارات. نحتاج إلى تفريغ قاعدة البيانات قبل وبعد اختباراتنا، فقط للتأكد من أنها فارغة تماماً. في الوقت نفسه، يتعين علينا إغلاق الاتصال بها يدوياً، حتى لا تبقى لدينا أي معالجات مفتوحة (open handlers) بعد الانتهاء من الاختبارات.
باستخدام اختبار الوحدة، تحققنا مما إذا كانت دالتنا تعمل كما هو مصمم. لذلك هنا كل ما علينا فعله هو اختبار ما إذا كانت دالتنا تتكامل مع طريقة save() من TypeORM ويتم تخزين مستخدمنا داخل قاعدة البيانات. نكتب دالة مساعدة تسمى getOneUserFromDb، والتي تفعل ما يقول اسمها. ثم نتحقق مما إذا كان البريد الإلكتروني صحيحاً وكذلك خاصية accountConfirmed، التي تم تعيينها في فئة الكيان (entity class) بالقيمة الافتراضية false.
اختبار شامل من البداية إلى النهاية (End-to-End Testing)
اختبار شامل من البداية إلى النهاية هو أسلوب لاختبار البرمجيات يستخدم لاختبار ما إذا كان تدفق التطبيق يعمل كما هو مصمم من البداية إلى النهاية. نقوم بهذا النوع من الاختبارات لضمان أن التطبيق سيعمل كما هو متوقع في موقف حقيقي.
حتى هذه النقطة، اختبرنا ما إذا كانت كلمة مرور المستخدم قد تم تجزئتها بشكل مناسب وما إذا تم حفظ كلمة المرور والبريد الإلكتروني داخل قاعدة البيانات. الآن نحتاج إلى اختبار عمليات التحقق الخاصة بنا على مستوى الطلب (request level). في متحكمنا (controller)، لدينا أنبوب تحقق (validation pipe) يختبر الحمولة الواردة (incoming payload) للتحقق مما إذا كان الكائن يتطابق مع CreateUserDto.

وهذه هي الاختبارات:

هنا اختبرنا ما سيحدث إذا حاولنا إنشاء مستخدم ولكن لم نرسل الكائن بالكامل أو أرسلنا خصائص بتنسيق غير صحيح. هذه بعض الأمثلة على الحالات الهامشية (corner cases) من دالتنا التي اختبرناها باستخدام 3 أنواع فقط من اختبارات البرمجيات.
الاختبارات اليدوية مقابل الاختبارات الآلية
حتى الآن، قمنا بكتابة اختباراتنا يدوياً – ولهذه الحالة كان ذلك مثالياً. ولكن كلما زادت الشيفرة لديك، أصبحت مجموعات الاختبار الخاصة بك أكثر تعقيداً وأكبر حجماً. على سبيل المثال، إذا كنت ستختبر نظام مصادقة (authentication system)، فسيتعين عليك محاكاة السلوك الكامل لمستخدم حقيقي. وسيتعين عليك إنشاء نماذج وهمية للطلبات والاستجابات (mock the requests and responses)، بما في ذلك ملفات تعريف الارتباط (cookies) والعديد من الأشياء الأخرى، فقط لبناء البيئة لاختباراتك. يمكن أن تستغرق مجموعة اختبار طويلة وقتاً طويلاً للتشغيل.
لحسن الحظ، لديك خيار آخر عندما يتعلق الأمر بالاختبار: الأدوات الآلية. تحتوي هذه الأدوات على وظائف مدمجة لك لمحاكاة البيئة بأكملها لاختباراتك، مما يجعل عملية الاختبار أسهل بكثير. يمكنك الذهاب إلى أبعد من ذلك واستخدام أدوات اختبار API الآلية لتطبيقك. هذه أدوات تأتي بخيارات إضافية تجعلها رائعة لاختبار التحميل (load testing)، واختبار الانحدار (regression testing)، وتقارير البيانات للمواقف الحقيقية. بالإضافة إلى أنها تحتوي على واجهة مستخدم جيدة تجعل كتابة الاختبارات أسهل بكثير.
الخلاصة التقنية
تُعد عملية اختبار الخدمات المصغرة خطوة لا غنى عنها لضمان جاهزية أي تطبيق للإنتاج. كما رأينا، لا يقتصر الأمر على تحديد الأخطاء فحسب، بل يمتد ليشمل تعزيز الأمان، وتحسين الأداء، وتقديم تجربة مستخدم موثوقة. إن الفصل الواضح بين أنواع الاختبارات – من اختبار الوحدة الذي يركز على أصغر المكونات، إلى اختبار التكامل الذي يضمن التفاعل السلس بين الوحدات، وصولاً إلى اختبارات شاملة من البداية إلى النهاية التي تحاكي سيناريوهات الاستخدام الحقيقية – يمثل حجر الزاوية في استراتيجية اختبار فعالة. ومع تزايد تعقيد الأنظمة، يصبح الانتقال نحو الأدوات والمنصات الآلية ضرورة حتمية لتسريع دورة التطوير، وتقليل الأخطاء البشرية، والحفاظ على جودة الشيفرة بشكل مستمر. الاستثمار في الاختبار ليس مجرد تكلفة، بل هو استثمار في استقرار المنتج ونجاحه على المدى الطويل.