دراسة حالة: CI/CD واختبارات الوحدات لثلاثة تطبيقات مصرفية بعلامة بيضاء في عشرة أسابيع
كيف نقلت Mobian Studio حزمة SDK مالية وثلاثة تطبيقات مصرفية بعلامة بيضاء من البناء اليدوي إلى نظام CI/CD واحد، وضاعفت اختبارات الوحدات قرابة ثلاث مرات.
تصف معظم مقالات CI/CD الشكل الذي ينبغي أن يكون عليه خط الأنابيب. أما هذا المقال فيصف خط أنابيب بنيناه فعلًا: لمنصة تقنية مالية تشغّل حزمة Android SDK الخاصة بها ثلاثة تطبيقات مصرفية بعلامة بيضاء (white-label).
عندما بدأنا، لم يكن لدى أيٍّ من المستودعات الستة نظام CI يعمل. كانت النسخ تُبنى يدويًا، وأرقام الإصدارات تُرفع يدويًا في commits، وكانت إحدى المكتبات موجودة فقط على جهاز مطوّر واحد. وبعد عشرة أسابيع، أصبحت حزمة SDK والتطبيقات الثلاثة للعملاء تتشارك نظام CI/CD واحدًا، وصار بالإمكان إعادة إنتاج كل نسخة للمختبِرين من وسم Git، وتضاعفت مجموعة اختبارات الوحدات في SDK قرابة ثلاث مرات.
في هذا المقال نستعرض نقطة الانطلاق، وتصميم خط الأنابيب، واستراتيجية الاختبار، والنتائج بتواريخها، والعمل الذي لا يزال مفتوحًا.
نقطة الانطلاق
العميل منصة تقنية مالية تقف وراء عدة تطبيقات مصرفية وتطبيقات دفع. وتضم عائلة المنتجات ما يلي:
-
حزمة Android SDK تتألف من نحو 29 وحدة Gradle، تحوّل بيانات JSON القادمة من الواجهة الخلفية إلى شاشات أصلية، وتتولى أيضًا المدفوعات والمقاييس الحيوية ومسح البطاقات.
-
ثلاثة تطبيقات مصرفية بعلامة بيضاء مبنية على تلك الحزمة، ولكل منها نموذجه الخاص في التفريع والإصدار.
-
مكتبة شبكات ومجموعة اختبارات شاملة (end-to-end) يملكها فريق ضمان الجودة.
في يوليو وأغسطس 2026، كان الوضع على النحو التالي:
-
كانت قوالب CI قديمة موجودة في مستودع SDK، لكن لم يكن شيء منها يعمل.
-
كانت رموز الإصدار (version codes) تُرفع يدويًا: نحو 95 commit من هذا النوع في أحد التطبيقات منذ 2025، و28 في تطبيق آخر.
-
لم يكن بالإمكان إعادة إنتاج النسخة من نسخة نظيفة للمستودع، لأن إحدى المكتبات كانت موجودة فقط في ذاكرة Maven المؤقتة المحلية لدى أحد المطوّرين.
-
بعض وحدات الاختبار في SDK لم تكن تُصرَّف أصلًا، وقد كشف ذلك أول تشغيل لـ CI.
-
كان لدى SDK ما مجموعه 1,122 اختبارًا مُنفَّذًا وتغطية إجمالية بنسبة 8.1%، بينما كان لدى التطبيق الرئيسي 56 اختبارًا.
📌 فكرة أساسية:
لم يكن أحد في الفريق يرتكب خطأ. ففي غياب خط أنابيب، تعيش معرفة الإصدار ببساطة في رؤوس الأشخاص وعلى حواسيبهم المحمولة، وفي مجال مصرفي خاضع للتنظيم يُعدّ ذلك خطرًا قائمًا بذاته.
القيود
كان على خط الأنابيب أن يتلاءم مع البنية التحتية الحقيقية للعميل، لا مع بنية مثالية:
-
GitLab مستضاف ذاتيًا، انتقل إليه العميل من gitlab.com في يوليو 2026.
-
مُشغِّل macOS مشترك واحد من دون Docker، تستخدمه أيضًا مشاريع أخرى في الشركة، مع مساحة عمل تبقى قائمة بين المهام.
-
عمليات بناء تمتد عبر مستودعين: تجمع كل عملية بناء للتطبيق بين فرع من التطبيق وفرع من SDK قد لا يكون قد دُمج بعد.
-
مجال مصرفي خاضع للتنظيم، يدفع نحو نسخ قابلة لإعادة الإنتاج، وإبقاء توقيع الإصدار خارج Git، وفحص التبعيات.
ما الذي بنيناه: خط أنابيب واحد لثلاثة تطبيقات
1. نموذج ويب واحد يوصل النسخة إلى المختبِرين
تبدأ نسخة المختبِرين من نموذج واحد في GitLab: اختر فرع التطبيق وفرع SDK ونوع البناء ومجموعات المختبِرين. ثم يشغّل خط الأنابيب ست مراحل:
-
التحضير: يحوّل فرع SDK إلى commit واحد، بحيث تستخدم كل مهمة لاحقة كود SDK نفسه تمامًا.
-
الاختبار: اختبارات الوحدات للتطبيق ولـ SDK، والتغطية، وفحص التبعيات.
-
البناء: ملف APK موقّع (إضافةً إلى ملف AAB لمتجر Play في الإصدارات)، مع أخذ بيانات اعتماد التوقيع من متغيرات CI.
-
التوزيع: عبر Firebase App Distribution إلى مجموعات المختبِرين المختارة.
-
الإشعار: تعليق في Jira على كل تذكرة مشمولة يتضمن رابط النسخة والتغطية، ونقل التذكرة إلى حالة “Ready for QA”.
-
الوسم: الوسم نفسه على commit التطبيق وcommit حزمة SDK.
2. كل نسخة للمختبِرين قابلة لإعادة الإنتاج
لأن الوسم يُوضع في المستودعين بعد التوزيع، فإن الوسم يعني “هذه النسخة بالتحديد وصلت إلى المختبِرين”. وسحب الوسم في المستودعين كليهما يعيد إنتاج النسخة. وقبل ذلك لم يكن هذا ممكنًا على الإطلاق.
3. قالب واحد ونقل آلي
تشغّل التطبيقات الثلاثة خط الأنابيب نفسه ذا المراحل الست. ولا يختلف بينها سوى عدد قليل من الثوابت، مثل تسمية Jira وبادئة الوسم ومجموعات المختبِرين، وهي موجودة في مهمة قالب مخفية واحدة. ونُقل خط الأنابيب إلى التطبيقين الثاني والثالث في أيام، رغم أن أحدهما يستخدم نموذج فروع الإصدار وكان فيه سبعة عوائق منفصلة يجب تذليلها.
4. بوابات الجودة حيث يعمل الناس أصلًا
-
نتائج الاختبارات في تبويب Tests ضمن طلب الدمج.
-
شارة تغطية المنطق في أداة طلب الدمج، وعلامات التغطية على الأسطر المتغيّرة.
-
SonarQube على الفرع الرئيسي.
-
OWASP Dependency-Check مع SBOM بصيغة CycloneDX في التطبيق الرئيسي.
5. مُشغِّل مشترك يُعامل على أنه غير موثوق
تعيش الأسرار في متغيرات CI فقط، ولا يُخزَّن شيء على المُشغِّل، وتنسخ كل مهمة حزمة SDK إلى مساحة عملها الخاصة (بعد أن حذفت مهام متوازية ذات مرة نسخ بعضها البعض)، وتتجاوز نسخ الإصدار الموقّعة الذاكرة المؤقتة للبناء دائمًا.
ما الذي بنيناه: استراتيجية اختبار تبدأ من البنية المعمارية
كانت التغطية هي المقياس، لكن العمل الحقيقي كان جعل الكود قابلًا للاختبار. وقد شكّلت أربعة قرارات النتيجة.
1. قِس الكود المهم
عرّفنا مجموعة المنطق: النطاق (domain)، والبيانات، والأدوات المساعدة غير المرتبطة بـ Android، ومحرّكَي الطلبات والأحداث، وفئات ViewModel الأساسية. أما كود الواجهة وربط DI والكود المولَّد فلا تزال تُدرج في التقارير، لكن من دون هدف. وهذا وحده غيّر نقطة الانطلاق الصادقة من 8% (قاعدة الكود بأكملها) إلى نحو 24% (المنطق).
2. طبّق مبدأ السقّاطة (Ratchet) على البنية المعمارية
تعمل سبع قواعد مؤتمتة للبنية المعمارية على هيئة اختبارات وحدات، مثل منع أنواع Android في كود المنطق ومنع حقن التبعيات العام. وتُجمَّد مخالفات اليوم في ملف أساس (baseline): فأي مخالفة جديدة تُفشل البناء، وكذلك أي مخالفة أُصلحت إلى أن تُحذف من ملف الأساس. وهكذا لا يمكن للدَّين التقني إلا أن يتقلص.
3. نقاط الفصل أولًا، والاختبارات ثانيًا
حصلت كل تبعية لـ Android أو لمورّد خارجي، مثل التحليلات والأذونات والمقاييس الحيوية وذاكرة الشاشات المؤقتة وتوقيع الطلبات، على واجهة صغيرة في طبقة النطاق ومحوِّل (adapter) خارج بوابة التغطية. وبعد ذلك صار المنطق يعمل على JVM العادية في أجزاء من الثانية، وأصبحت كتابة الاختبارات قليلة التكلفة.
4. ثبّت السلوك ولا تصلحه بصمت
تسجّل اختبارات طبقة البيانات ما يفعله الكود اليوم. وكل سلوك مشكوك فيه يتحول إلى سؤال لفريق المنتج أو الواجهة الخلفية بدلًا من تغيير صامت، وقد بلغ عدد الأسئلة المفتوحة حتى الآن 76 سؤالًا. وعندما يُتخذ قرار، يفشل اختبار التثبيت عمدًا ويُحدَّث مع الإصلاح.
النتائج
جميع الأرقام أدناه صحيحة حتى 5 أكتوبر 2026، وقد قُرئت من واجهة GitLab البرمجية لخطوط الأنابيب أو أُحصيت من المستودعات.
| المقياس | قبل | بعد |
| المستودعات التي لديها CI يعمل | 0 من 6 | 4 من 6 (SDK + 3 تطبيقات) |
| من الدمج إلى المختبِرين | بناء يدوي ورفع يدوي لأرقام الإصدار | نحو 6–8 دقائق، بأتمتة كاملة |
| نسخ المختبِرين عبر خط الأنابيب | — | 101 منذ 8 سبتمبر |
| تشغيلات خطوط الأنابيب | — | نحو 530 |
| اختبارات الوحدات في SDK | 1,141 (11 سبتمبر) | 3,246 (×2.8) |
| تغطية المنطق في SDK | نحو 24% | 70.9% |
| الاختبارات الفاشلة تمنع الدمج | لا | نعم، في SDK والتطبيق الرئيسي |
| استيرادات Android في كود المنطق | 298 | 192 (−36%) |
| إعادة إنتاج نسخة للمختبِرين | غير ممكنة | سحب وسم واحد في المستودعين |
أسرع مرحلة: ارتفعت تغطية المنطق من 30.7% إلى 70.9% في سبعة أيام (من 28 سبتمبر إلى 5 أكتوبر)، حين غُطّيت طبقة البيانات بأكملها عبر 13 تذكرة. ولم تحدث هذه القفزة إلا لأن نقاط الفصل في البنية المعمارية كانت جاهزة مسبقًا.
كما استردت الاختبارات تكلفتها قبل بلوغ هدف التغطية. فقد كشفت كتابة اختبارات التثبيت عن أعطال عند تلقي بيانات خادم مشوّهة، وثغرات في التوافق المستقبلي في الإصدارات الأقدم من التطبيق، وحلقة إعادة محاولة مفرطة في تحديث الرمز المميز (token)، وكلها الآن قيد المتابعة ومرتّبة حسب الأولوية.
ما الذي لا يزال مفتوحًا
نفضّل نشر الثغرات إلى جانب النتائج:
-
لا يوجد حد أدنى للتغطية بعد: يُمنع الدمج عند فشل الاختبارات، لا عند انخفاض التغطية. وقد تجاوزنا هدفَي 40% و60%، ولا يزال هدف 80% أمامنا.
-
لا تزال الاختبارات في تطبيقين غير مانعة، ولم يعتمد هذان التطبيقان بعد مجموعة أدوات الاختبار المشتركة.
-
مستودعان لا يملكان CI، ولا تزال الاختبارات الشاملة تعمل على جهاز محلي.
-
المُشغِّل الوحيد نقطة فشل وحيدة: ففي يومين انتظرت المهام حتى انتهت مهلتها أثناء عدم توافره. والخطوة التالية مُشغِّل ثانٍ أو مجموعة مُشغِّلات ذات توسع تلقائي.
دليل العمل الذي نستخدمه الآن مع عملاء آخرين
-
ثبّت التبعيات المشتركة وضع وسمًا على كل مستودع يشارك في عملية البناء، بحيث يمكن إعادة إنتاج أي نسخة للمختبِرين.
-
استخدم قالب خط أنابيب واحدًا لكل عائلة منتجات، مع اختزال الفروق إلى عدد قليل من الثوابت.
-
أغلق الحلقة حيث يعمل الناس أصلًا: يذهب رابط النسخة والتغطية إلى تذكرة Jira وإلى طلب الدمج.
-
أعد تعريف التغطية قبل أن تطاردها، كي يكون الرقم صادقًا والهدف قابلًا للتحقيق.
-
تقدّم بمبدأ السقّاطة لا بالتغيير الشامل دفعة واحدة: جمّد مخالفات اليوم وأفشل البناء عند أي مخالفة جديدة.
-
عامل المُشغِّل المشترك على أنه غير موثوق: الأسرار في متغيرات CI، ونسخ الإصدار لا تُخزَّن مؤقتًا أبدًا.
الخلاصة
في عشرة أسابيع، انتقلت عائلة منتجات في التقنية المالية من ملفات APK مبنية يدويًا إلى نظام CI/CD واحد: نحو 530 تشغيلًا لخطوط الأنابيب، و101 نسخة مؤتمتة للمختبِرين، والانتقال من الدمج إلى المختبِرين في دقائق، ومجموعة اختبارات في SDK نمت من 1,141 إلى 3,246 اختبارًا مع تغطية للمنطق بنسبة 70.9%. ولم يتطلب أي من ذلك مورّد CI جديدًا، بل خط أنابيب مصمّمًا حول البنية التحتية الحقيقية للعميل وبنية معمارية تتيح الاختبار.
في Mobian هذا هو نوع العمل الذي نحب أن نعرضه بدلًا من أن نصفه. إذا كانت إصداراتك لا تزال تعتمد على حاسوب شخص واحد، فيمكننا أن نبدأ بتدقيق عملية البناء والإصدار الحالية لديك ونخبرك بصراحة بما يستحق الأتمتة أولًا.
الأسئلة الشائعة
كم استغرق إعداد CI/CD؟
نحو عشرة أسابيع، من أواخر يوليو إلى أوائل أكتوبر 2026. نجح أول تشغيل لخط أنابيب SDK بعد نحو شهر. وبدأ إرسال نسخ المختبِرين عبر خط الأنابيب في أوائل سبتمبر. وبعد ذلك استغرق نقل خط الأنابيب نفسه إلى التطبيقات المصرفية الأخرى أيامًا لا أسابيع.
لماذا نقيس تغطية المنطق بدلًا من التغطية الإجمالية؟
تحتسب التغطية الإجمالية كود الواجهة وربط حقن التبعيات والكود المولَّد، حيث لا تضيف اختبارات الوحدات قيمة تُذكر. أما تغطية المنطق فلا تحتسب إلا كود النطاق (domain) والبيانات والمحرّكات، حيث تكلّف الأخطاء مالًا. وهذا يجعل الرقم صادقًا والهدف قابلًا للتحقيق. في هذا المشروع كانت نقطة الانطلاق 8.1% تغطية إجمالية، لكن نحو 24% تغطية للمنطق، قبل كتابة أي اختبار جديد.
هل عمل خط الأنابيب على CI سحابي؟
لا. يشغّل العميل GitLab مستضافًا ذاتيًا مع مُشغِّل macOS مشترك واحد، لذا صُمّم خط الأنابيب لذلك: تحصل كل مهمة على نسخة SDK خاصة بها، وتعيش الأسرار في متغيرات CI فقط، ولا تؤخذ نسخ الإصدار الموقّعة أبدًا من الذاكرة المؤقتة المشتركة للبناء.
هل انتهى العمل؟
ليس بالكامل، ونفضّل أن نقول ذلك صراحة. فالحد الأدنى للتغطية (منع الدمج عند انخفاض التغطية) لم يُبنَ بعد، والاختبارات في تطبيقين من التطبيقات لا تزال غير مانعة، ويظل المُشغِّل الوحيد خطرًا على السعة والتوافر. ولا يزال هدف تغطية المنطق بنسبة 80% أمامنا.
هل يصلح هذا النهج لمنتج iOS أو منتج متعدد المنصات؟
المبادئ تنطبق: ثبّت التبعيات المشتركة، وضع وسمًا على كل نسخة للمختبِرين، وقِس الكود المهم، وأضف الاختبارات بعد أن تسمح بها البنية المعمارية. كان هذا المشروع بالتحديد لـ Android فقط، لذا تختلف الأدوات على iOS (Xcode وSwift Package Manager وXCTest)، لكن دليل العمل يبقى نفسه.