اختبار التكامل من خدمات micros على Scala

اختبار وحدة كبيرة ، ولكن واحدة ليست كافية. غالبًا ما تريد التأكد من أن التطبيق قيد التشغيل سوف يعمل. اختبار التكامل يأتي لانقاذ. يتم استخدامه بشكل متزايد لاختبار الخدمات ، ويتيح لك Docker إدارة بيئة الاختبار بسهولة. لكن ، كما هو الحال دائمًا ، الأمور ليست بهذه البساطة عندما يكون هناك المزيد من الخدمات الدقيقة والتبعيات.

أخبر Yuri Badalyants من RIT ++ كيف يختبرون في 2GIS مجموعة من الخدمات الكبيرة وحديقة للحيوانات كاملة التكنولوجيا. في النهاية ، يتم استكمال وتحديث إصدار هذا التقرير تحت الإشراف الدقيق للمتحدث: ما هي الخيارات التي جربتها ، وما توصلت إليه ، والمشاكل التي لا يتعين عليك حلها الآن. سيكون حول Docker ، Testcontainers ، وكذلك حول Scala.


نبذة عن المتحدث: بدأ يوري باداليانتس (@ LMnet ) حياته المهنية في عام 2011 كمطور ويب ، وعمل مع PHP و JavaScript و Java. الآن يكتب على سكالا في 2GIS.

كازينو


توفر شركة 2GIS خرائط ملائمة للمدينة وأدلة الشركة منذ 20 عامًا ، ومؤخراً لدينا إصدار جديد مع خريطة غير محدودة لروسيا. سوف أخبرك بالخبرة المكتسبة أثناء عملي في فريق الكازينو. يشارك هذا الفريق في ثلاثة مجالات رئيسية:

  • الإعلان - أي المعلنين لإظهاره ، وأي شيء يجب إخفائه ، ورفعه وكيفية خفض التصنيف.

  • يرتبط BigData بالإعلان وتخصيصه ، وكذلك بناء التحليلات والمقاييس.
  • Crawler هو برنامج يبحث عن مؤسسات على الإنترنت لإضافتها تلقائيًا إلى قاعدة البيانات.

هذه المجالات الثلاثة هي المهام الرئيسية ، والتي ، بدورها ، لديها عدد كبير من المهام الفرعية. حاليًا ، هناك أكثر من 25 خدمة ميكروية مكتوبة في سكالا. هذا هو رمزنا على وجه الحصر ، ومع ذلك نستخدم أيضًا أنظمة الجهات الخارجية ، على سبيل المثال ، PostgreSQL و Cassandra و Kafka. نقوم بتخزين البيانات في Hadoop ومعالجتها في Spark. بالإضافة إلى ذلك ، نستخدم طرق التعلم الآلي التي يوفرها فريق علوم البيانات.

نتيجة لذلك ، لدينا عدد كبير من الخدمات والخدمات المصغرة ، وعدد كبير من التبعيات ، وبالطبع ، كل هذا يحتاج إلى اختبار بطريقة ما.

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

اختبارات التكامل


تحل كل خدمة microservice تم تطويرها بواسطة فريق Casino مشكلة أعمالها وتقع في مستودع منفصل في GitLab. ستركز هذه المقالة على اختبار التكامل داخل أحد هذه المستودعات (خدمة microservice) مع التبعيات المغلقة ، والتي تقع على عاتق المطورين أنفسهم. يقوم فريق ضمان الجودة باختبار تفاعل الخدمات المصغرة ، ولن أتطرق إلى هذا الموضوع.

عندما انضممت إلى الفريق لأول مرة ، في نهاية عام 2016 ، كان هناك مخطط اختبار التكامل التالي تقريبًا:


  1. يقوم المطور بدفع الكود الخاص به في GIT ، وبعد ذلك يتم إدخال رمز الخدمة الميكروية إلى TeamCity. TeamCity يبدأ بناء رمز وتشغيل الاختبارات.
  2. يأخذ TeamCity ملف التكوين (config) من Chef (نظام إدارة التكوين يشبه Ansible ، مكتوب فقط في روبي). الشيف يخدم أيضا لأتمتة النشر. عندما يكون لدي 100 جهاز ، لا أريد الذهاب إلى كل منها وتثبيت ما أحتاج إليه على SSH ، ويسمح لي الشيف بأتمتة هذا.
  3. يقوم TeamCity بجمع ملف jar (نظرًا لأننا نكتب في Scala ، فإن الأداة التي ننشرها هي jar) ، ثم يقوم البرنامج بتحميلها في بيئة CI. يتم نشر تطبيقنا هناك ، وهناك أيضا بعض التبعيات. في الرسم التخطيطي ، تظهر إحدى التبعيات كقاعدة بيانات. يمكن أن يكون هناك أكبر عدد ممكن من التبعيات ، وبفضل الشيف ، يعرف تطبيقنا عنها ويبدأ بالتفاعل معهم.
  4. بعد ذلك ، يطلق TeamCity SBT (هذا هو نظام الإنشاء الخاص بنا ، حيث يتم تشغيل التجميع والاختبارات) ويقوم بإجراء الاختبارات بأنفسهم. تتشابه نسبياً مع اختبارات الوحدات ، ولكنها تعمل بشكل أساسي على هذا المبدأ: انتقل عبر http إلى عنوان محدد ، وتحقق من بعض الطرق وشاهد ما يُرجع ؛ أو القيام ببعض التحضير ، ثم معرفة ما إذا كان ما هو مطلوب قد عاد.

ماذا يمكن أن يقال عن مثل هذا المخطط؟ الأهم من ذلك ، أنه يعمل. عندما يتم إعداد كل شيء ، يصبح إجراء الاختبارات أمرًا سهلاً ، حيث يبدو أنها اختبارات الوحدات. لكن الإيجابيات تنتهي هناك.

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

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

لا يمكن اختبار البدء والتوقف وإعادة التشغيل . سأشرح لماذا هذا ضروري: كل تطبيقاتنا تلتزم بمنطق ما يسمى بالإغلاق الرشيق ، أي عندما نحصل على SIGTERM ، لا نوقف العملية في الوسط ، ولكن نعترض هذه الإشارة ونفهم أننا بحاجة إلى إيقاف تشغيل البرنامج. في هذه المرحلة ، يتم تشغيل منطق معين ، على سبيل المثال ، تتم معالجة طلبات HTTP التي "في الرحلة" ، أو إذا كنا نعمل مع Kafka ، فنحن نلتزم بجميع العمليات - وبعبارة أخرى ، نقوم بتنفيذ إجراءات معينة حتى نتمكن من إكمال العمل بأمان ، و ثم ، عندما يتم كل شيء ، وإيقاف.

هذا المنطق ليس بسيطًا دائمًا ، ويمكنك اختباره باستخدام هذا المخطط يدويًا فقط ، لأنه من الاختبارات لا نتحكم في دورة حياة التطبيق. اتضح أن TeamCity قام بنشر شيء ما بطريقة ما من خلال Chef ، في حين أن الاختبارات في مرحلة مختلفة ولا تعرف كيف يتم نشر التطبيق.

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

GitLab CI + Docker


بمرور الوقت ، تحول هذا المخطط إلى مخطط آخر: GitLab CI و Docker . لم يحدث هذا لأن المخطط السابق لم يكن مثاليًا ، ولكن لأن الشركة غيرت مسارها قليلاً من حيث التنظيم الإداري.

سابقا ، كل فريق ، ولدينا الكثير منهم ، كما أردنا أو كيف يمكننا ، ونشر أعماله. على سبيل المثال ، كان لدينا TeamCity و Chef ، ويمكن للفرق الأخرى استخدام Jenkins أو Ansible.

الآن نحن نتحرك نحو السحابة المحلية و Kubernetes ، وهناك فريق منفصل يدير كل هذا ، كل من GitLab CI و Kubernetes. فرق أخرى فقط استخدام هذا كخدمة. هذا أكثر ملاءمة لأنك لا تحتاج إلى إدارة كل هذا يدويًا.

باستخدام Kubernetes ، قمنا بنشر المخطط التالي:


  1. بدلاً من TeamCity ، يتم استخدام Gitlab CI الآن.
  2. يقوم GitLab CI بإنشاء صورة لرسو السفن ونشرها على Kubernetes. يتم تخزين التكوين الآن مباشرةً في المستودع ، وليس بشكل منفصل في Chef ، لذلك لن تحتاج للعمل مع خدمة تهيئة تابعة لجهة خارجية للنشر.
  3. يتم رفع التبعيات مسبقًا ، أيضًا في Kubernetes.
  4. ثم GitLab CI تطلق SBT والاختبارات في خطوة منفصلة.

كل شيء مشابه تمامًا للمخطط السابق ولا يختلف اختلافًا جوهريًا عنه ، أي أن إيجابيات وسلبيات ستكون هي نفسها تمامًا ، لكن Docker يظهر.

مع عامل الميناء ، يمكنك القيام بأشياء أكثر متعة مختلفة ، وواحد منها عبارة عن عامل إنشاء.

عامل الميناء-يؤلف


هذا نوع من "التراكب" على Docker ، والذي يسمح لك بتشغيل عدة صور لرسو السفن ككيان واحد.

ومن الأمثلة الجيدة على ذلك ، حيث يساعدك عامل الرصيف فعلاً في الخروج ، هو كافكا. إنها تحتاج إلى ZooKeeper للتشغيل. إذا قمت برفع Kafka و ZooKeeper دون إنشاء عامل ميناء ، فأنت بحاجة إلى رفع ZooKeeper بشكل منفصل في عامل الميناء ، بشكل منفصل - Kafka ، والحفاظ على حاويات عامل النقل هذه متناسقة. هذا ليس ملائمًا جدًا ، ويتيح لك عامل التهيئة إمكانية وصف كلتا الحاويتين في أحد ملفات docker-compose.yml واستخدام الأمر البسيط docker-compose run Kafka Kafka لرفع Kafka و ZooKeeper.

يمكنك بناء اختبارات التكامل على إنشاء عامل ميناء. دعونا نرى كيف سيبدو.


  1. مرة أخرى ، ادفع كل شيء في GitLab.
  2. GitLab CI تطلق عامل إنشاء.
  3. في التطبيق docker-compose ، يرتفع التطبيق ، وكل التبعيات و SBT ترتفع ، و SBT يقود الاختبارات لهذا التطبيق - كل شيء يحدث داخل الإنشاء docker.

بفضل هذا المخطط ، ليست هناك حاجة للحفاظ على بيئة منفصلة والتبعيات ، لأن كل شيء يذهب مباشرة إلى عداء GitLab CI ، حيث يجب أن يكون عامل الالتحام ورسو السفن فقط. خلال البداية ، سوف يضخ الصور الضرورية ويديرها.

بالإضافة إلى ذلك ، يمكنك اختبار فروع مختلفة في نفس الوقت ، لأن كل شيء يحدث على عداء.

أصبح من السهل الآن تهيئة البيئة محليًا ، ولكن لا يزال يتعين عليك تنسيق عدة أماكن. الشيء هو أنه الآن ، عندما نقوم بالتكوين المحلي ، لا نحتاج إلى وضع كل شيء على الجهاز المحلي ، كل شيء مكتوب في ملف docker-compose.yml. وبالتالي ، يجب عليك التهيئة في مكانين مختلفين - وهذا هو عامل الترسيم compose.yml وتهيئة اختباراتنا.

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

عامل ميناء يؤلف 2


في أيام docker-compose 2 ، docker-compose.yml بدا الملف كما يلي:

 version: '2.1' services: web: build: . depends_on: db: condition: service_healthy redis: condition: service_started redis: image: redis db: image: db healthcheck: test: "some test here" 

يتم تسجيل الخدمات هنا ، أي ما سنقوم برفعه كجزء من إنشاء عامل النقل هذا. في هذه الحالة ، أخذت للتو مثالًا من وثائق إنشاء عامل النقل. هناك ثلاث خدمات: الويب ، redis و db (قاعدة بيانات).

الويب هو تطبيقنا ، و redis و db نوع من التبعيات.

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

أيضا ، هناك condition الشرط. بالنسبة إلى redis ، يتم تشغيل service_started ، مما يعني أنه إلى أن يبدأ redis ، لن تحاول الحاوية بدء تشغيل تطبيق الويب.

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

على سبيل المثال ، نستخدم PostgreSQL ، والذي يستخدم امتداد PostGIS ، ويحتاج إلى بعض الوقت للتهيئة. عندما نطلق حاوية الإرساء ، لا يمكننا العمل على الفور مع ملحق postgis - نحتاج إلى الانتظار لبدء التهيئة. لذلك ، SELECT PostGIS_Version(); فقط استفسارات SELECT PostGIS_Version(); إلى SELECT PostGIS_Version(); . إلى أن تتم تهيئة الامتداد ، سوف يرسل الطلب خطأً ، وعند تهيئة الامتداد ، سيبدأ إرجاع الإصدار. هذا أمر مريح ومنطقي - سنقوم أولاً برفع جميع التبعيات ، ثم التطبيق .

عامل ميناء يؤلف 3


عندما خرج عامل الإرساء 3 ، بدأ استخدامه.

ولكن في الوثائق الخاصة به ، ظهر عنصر في تغيير المنطق depend_on. قرر مطورو عامل ميناء أن وصف الرسم البياني التبعية كان كافياً. هذا يعني أنه عند بدء docker-compose run web سيبدأ كل من التطبيق نفسه وديسيبل في نفس الوقت.



تشير الفقرة التالية في الوثائق إلى أن "يتوقف" لم يعد شرطًا.

وبالتالي ، إذا كنت لا تزال ترغب في الحصول على الوظيفة التي تم استخدامها في الإصدار الثاني ، فسوف يتعين عليك أخذ كل شيء بين يديك.

تقدم صفحة التحكم في بدء التشغيل العديد من الحلول. الخيار الأول هو استخدام wait-for-it.sh .

يبدو عامل التجميع الآن مختلفًا قليلاً:

 version: '3' services: web: build: . depends_on: [ db, redis ] redis: image: redis command: [ "./wait-for-it.sh", ... ] db: image: redis command: [ "./wait-for-db.sh", ... ] 

depends_on على مجموعة فقط ، لا توجد شروط.

في تبعياتنا ، نعيد تعريف الأمر ، أي أنه في حالة عامل الرصيف ، يمكنك إرفاق أمر يبدأ به حاوية عامل النقل.

هناك يجب أن نكتب wait-for-it.sh ، وشيء آخر. بدلاً من النقاط الثلاث المذكورة في المثال أعلاه ، يجب أن نكتب ما نحتاج إلى الانتظار ، بالإضافة إلى الأمر الأصلي الذي يقوم بتشغيل حاوية الإرساء.

للقيام بذلك ، تحتاج إلى العثور على ملف عامل ميناء ، ونسخ أمر redis من هناك ولصقه ، والشيء نفسه ينطبق على قاعدة البيانات. يتمثل الطرح الضخم في أن التجريد ينهار - لا أريد أن أعرف الأمر الذي يطلق حاوية الإرساء. يمكن أن تكون هذه الأوامر غير تافهة ، معقدة للغاية ، لكنني لا أريد أن أزعج ، أريد فقط إدخال docker run وهذا كل شيء.

أنا شخصياً لا أحب هذا الحل حقًا ، ولكن كان لدينا عدد من الخدمات التي تعمل مثل هذا.

النصي على رأس عامل التأسيس


ثم قررت أن الوقت قد حان لـ " بناء الدراجات" ، وكان لدي عامل رصيف- run.sh :

 version: '3' services: postgres: ... my_service: depends_on: [ postgres ] ... sbt: depends_on: [ my_service ] ... 

دعني أعطيك مثالًا شبه واقعي: يوجد postgres في docker-compose.yml ، هناك تطبيق my_service ، والذي يعتمد على postgres ، و SBT ، حيث يتم تشغيل الاختبارات والتي تعتمد على الخدمة التي أعمل بها.

أقوم بتشغيل البرنامج ليس من خلال docker run الإرساء ، ولكن من خلال البرنامج النصي - doker-compose-run.sh.

أولاً ، يبدأ التبعية الأعمق أولاً ، في حالتي ، فهي مرحلة ما بعد المدرسة. يبدأ البرنامج النصي التبعية في وضع "الخفي" ، أي أنه لا يحظر المحطة:

 docker-compose up -d postgres 

ثم انتظر الشرط أن يكون راضيا عن وظيفة wait_until. هذا هو تقريبا مثل الانتظار- it-sh ، فقط ، إذا جاز التعبير ، بأسلوب ضروري. أثناء تهيئة PostGIS ، يتم حظر الجهاز الطرفي ، أي أن البرنامج ينتظر أيضًا ، وإذا لم ينتظر ، يتم إلقاء خطأ وتوقف الاختبارات عن العمل.

 wait_until 10 2 docker-compose exec -T postgres psql 

عند تهيئة PostGIS ، انتقل إلى الخطوة التالية وقم بالشيء نفسه مع الخدمة. بالنسبة له ، الاختبار أبسط قليلاً: يجب أن يكون المنفذ 80 ملزماً.

 docker-compose up -d my_service wait_until 10 2 docker-compose exec -T \ my_service sh -c "netstat -ntlp | grep 80 || exit 1" 

الخطوة الأخيرة هي تشغيل SBT من خلال الأمر run ، حيث يتم تشغيل الاختبارات.

 docker-compose run sbt down $? 

وبالتالي ، يتم رفع كل شيء في الترتيب الصحيح ، ولكن يدويا.

في النهاية ، تسمى الوظيفة down ، والتي تقبل نتيجة الأمر السابق. إذا كانت "0" ، فقد مرت الاختبارات ، وسنعمل على إيقاف تشغيل عامل الإرساء ؛ خلاف ذلك ، نحن أولاً "بصق" السجلات لمعرفة ما حدث من خطأ ، وعندها فقط قم بإيقاف تشغيل عامل الإرساء.

 function down { echo "Exiting with code $1" if [[ $1 -eq 0 ]]; then docker-compose down exit $1 else docker-compose logs -t postgres my_service docker-compose down exit $1 fi } 

مثل هذا المخطط يعمل ، لكنه لا يتقيد بشكل جيد. سيتعين على كل خدمة أن تصف عامل التشغيل الخاص بها - run.sh بمنطقها الخاص. بالإضافة إلى ذلك ، يمتد تكوين التشغيل بين docker-compose-run.sh و docker-compose.yml. حسنًا ، بشكل عام ، يبدو أننا لا نستخدم عامل بناء السفن ، ولكننا نكافح مع أوجه القصور فيه.

تشغيل عامل ميناء من التعليمات البرمجية


عندما تم إنشاء المخطط السابق ، اعتقدت: إذا كان لدي بالفعل كل شيء في عامل الميناء ، فلماذا لا يتم تشغيله من التعليمات البرمجية. بدأت أبحث عن حل ووجدت العديد من الخيارات.

الخيار الأول هو ببساطة استخدام عميل عامل ميناء . يوجد عميلان رئيسيان للإرساء في عالم JVM: عامل الإرساء - جافا وسبوتيفي - عامل الإرساء .

يسمح لك عميل docker بتشغيل أوامر docker مباشرة من الكود باستخدام API. أي أنه بدلاً من تسلسل السلاسل لإنشاء أوامر مثل `docker run ...` ، يمكنك ببساطة تكوين مثل هذا الأمر في التعليمات البرمجية وتشغيله. هو أكثر ملاءمة.

تعمل هذه الطريقة بشكل جيد ، وبالتأكيد يمكنهم القيام بكل شيء ، ومع ذلك ، فهذا مستوى منخفض جدًا. يجب أن أقوم بتكوين نظري التماثلي ، وهو مهمة كبيرة للغاية.

الخيار التالي هو مكتبة docker-it-scala ، التي تلتف مع كل من هؤلاء العملاء وتتيح لك اختيار الخلفية التي تريد استخدامها. يمكنها تشغيل الحاويات التي تحتاجها.

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

لم يعجبني هذا الخيار أيضًا ، واصلت البحث ووجدت Testcontainers . أود أن أخبركم المزيد عن هذا.

Testcontainers


هذا هو نوع من مكتبة جافا لإطلاق واختبار حاويات الرصيف. هناك واجهة سكالا ، testcontainers-scala. من خارج الصندوق ، هناك عدد من الخدمات الشائعة ، على سبيل المثال ، PostgreSQL ، MySQL ، Nginx ، Kafka ، Selenium. يمكنك تشغيل أي حاويات أخرى. تحتوي المكتبة على واجهة برمجة تطبيقات بسيطة ومرنة إلى حد ما ، وسوف أتناولها بمزيد من التفصيل.

حاويات محددة مسبقا


لذلك ، كيفية التعامل مع الحاويات المعرفة مسبقًا ، الموجودة في المكتبة: في الواقع ، كل شيء بسيط للغاية ، حيث يتم تمثيل الحاويات ككائنات:

 val pgContainer: PostgreSQLContainer = PostgreSQLContainer("postgres:9.6") pgContainer.start() val pgUrl: String = pgContainer.jdbcUrl val pgPort: Int = pgContainer.mappedPort(5432) pgContainer.stop() 

في هذه الحالة ، نقوم بإنشاء PostgreSQLContainer ، يمكننا أن نبدأ ذلك ونبدأ العمل معه. بعد ذلك ، نحصل على jbdcUrl ، حيث يمكنك الاتصال بـ PostgreSQL. بعد ذلك نحصل على mappedPort .

هذا يعني أن PostgreSQL يتمسك بها من ميناء docker 5432 ، و Testcontainers يرى هذا المنفذ ويقوم بتعيينه تلقائيًا لبعض المنافذ العشوائية. هذا هو ، من الاختبارات التي نراها ، على سبيل المثال ، 32422. تحدث المهمة تلقائيًا.

حاوية مخصصة


طريقة العرض التالية ، الحاوية المخصصة ، بسيطة للغاية أيضًا:

 class GenericContainer( imageName: String, exposedPorts: Seq[Int] = Seq(), env: Map[String, String] = Map(), command: Seq[String] = Seq(), classpathResourceMapping: Seq[(String, String, BindMode)] = Seq(), waitStrategy: Option[WaitStrategy] = None ) ... 

هناك GenericContainer تحتاج منه إلى وراثة وتجاوز عدد من الحقول. تأكد من تعيين imageName فقط - هذا هو اسم الحاوية التي نريد إنشاءها.

يمكنك تعيين exposedPorts : تلك المنافذ التي ستتمسك بها الحاوية. في env ، يمكنك تعيين متغيرات البيئة ، ويمكنك أيضًا تعيين command لتشغيل.

classpathResourceMapping يسمح لك برمي الموارد من classpath في حاوية عامل الميناء. هذا مناسب جدًا ، على سبيل المثال ، إذا كان تكوين التطبيق موجودًا مباشرةً في موارد الاختبار. يمكنك ببساطة تعيين الداخل ، والتطبيق داخل عامل ميناء يحصل على الوصول إلى هذا التكوين.

waitStrategy هو شيء مريح للغاية تم فقده في برنامج waitStrategy 3 ، في الحقيقة إنه HealthCheck. هناك العديد من waitStrategy المحددة مسبقًا ، على سبيل المثال ، يمكنك الانتظار حتى يحدث ربط المنفذ ، أو ستعود طريقة http المحددة إلى 200. ولكن يمكنك كتابة أي من HealthCheck الخاص بك.

نظرًا لأنك تكتب HealthCheck ببساطة في التعليمات البرمجية الخاصة بك ، يمكنك أولاً استخدام لغة عادية ، وليس باش ، وثانياً ، أي مكتبات متاحة من التعليمات البرمجية الخاصة بك: إذا كنت ترغب في جعل HealthCheck مخصص في Cassandra - خذ برنامج التشغيل والكتابة أي HealthCheck.

تشغيل الاختبارات


والآن قليلاً عن كيفية إجراء الاختبارات:

 class PostgresqlSpec extends FlatSpec with ForAllTestContainer { override val container = PostgreSQLContainer() "PostgreSQL container" should "be started" in { Class.forName(container.driverClassName) val connection = DriverManager .getConnection(container.jdbcUrl, container.username, container.password) // test some stuff } } 

سأتحدث عن ScalaTest ، المعيار الفعلي للاختبار في عالم Scala.

على سبيل المثال ، نريد أن نكتب اختبارات Postgres. قم بإنشاء اختبار PostgresqlSpec ForAllTestContainer من ForAllTestContainer . هذه سمة تقدمها المكتبة. ستبدأ الحاويات اللازمة قبل كل الاختبارات وتوقفها بعد كل الاختبارات. أو يمكنك استخدام ForeachTestContainer ، ثم تبدأ الحاويات قبل كل اختبار وتتوقف بعد كل منها.

ثم تحتاج إلى إعادة تعريف الحاوية. يمكن القيام بذلك عن طريق تجاوز خاصية container . في حالتي ، أنا أستخدم PostgreSQLContainer .

ثم نكتب الاختبارات. في المثال ، أقوم بإنشاء اتصال وأخذ jdbcUrl واسم المستخدم وكلمة المرور وكتابة اختبارات محددة وإرسال الطلبات.

عادة ، تتطلب اختبارات التكامل عدة حاويات. يمكنني إنشائها باستخدام MultipleContainers :

 val pgContainer = PostgreSQLContainer() val myContainer = MyContainer() override val container = MultipleContainers(pgContainer, myContainer) 

بمعنى ، أقوم بإنشاء حاويات وإضافتها إلى حاويات MultipleContainers ، واستخدامها container .

نظام تشغيل الاختبارات مع Testcontainers كالتالي:



  1. ادفع الكود في GitLa.
  2. عداء GitLab CI يطلق SBT.
  3. SBT يدير الاختبارات. داخل الاختبارات ، يتم إطلاق تطبيقنا والتبعيات.

مزايا هذا المخطط:

  • لا حاجة للحفاظ على بيئة منفصلة والتبعيات ، كل شيء يحدث على عداء.
  • يمكنك اختبار فروع مختلفة في نفس الوقت.
  • يمكنك اختبار بدء التشغيل وإيقافه وإعادة تشغيله ، لأنه يمكننا التحكم في دورة حياة التطبيق (كل شيء يبدأ مباشرةً في رمز الاختبار).
  • هناك HealthChecks مرنة التي كانت تفتقر إلى حد كبير.
  • لا توجد ملفات * .sh في المستودع ، يمكنك تكوين الاختبارات في التطبيق بمرونة كما تريد.
  • بفضل classpathResource Mapping ، يمكنك استخدام نفس التكوين مع كل من الاختبارات والتطبيق.
  • يمكنك تكوين الاختبارات من التعليمات البرمجية.
  • كل هذا يسير بسهولة على حد سواء على CI ومحليا ، لأن هذه مجرد اختبارات تبدو وتعمل كاختبارات وحدة ، كل شيء فقط يرتفع في حاوية الرصيف.

لقد اتضح أن كل شيء على نحو سلس وجيد بشكل مثير للريبة ، لكن هذا فقط للوهلة الأولى ، في الواقع ، واجهنا عددًا من المشاكل.

حاويات تابعة


المشكلة الأولى التي واجهناها هي الحاويات التابعة . دعنا نقول أن هناك نوعا من الاختبار:

 class MySpec extends FlatSpec with ForAllTestContainer { val pgCont = PostgreSQLContainer() val appCont = AppContainer(pgCont.jdbcUrl, pgCont.username, pgCont.password) override val container = MultipleContainers(appCont, pgCont) // tests here } 

يتم تشغيل postgres و AppContainer. يتم تمرير appContainer من postgres jdbcUrl ، اسم المستخدم وكلمة المرور للاتصال. بعد ذلك ، يتم إنشاء MultiContainers ووصف الاختبار نفسه.

أدير البرنامج وأرى خطأ:

 Exception encountered when invoking run on a nested suite - Mapped port can only be obtained after the container is started 

النقطة المهمة هي أنه لا يمكن أخذ المنفذ المخصص حتى يتم تشغيل الحاوية. لماذا يحدث هذا؟

الحقيقة هي أن ForAllTestContainer أو ForEachTestContainer تشغيل الحاويات مباشرة قبل الاختبارات ، وليس في الوقت الحالي عندما أقوم بإنشاء مثيلات حاوية. اتضح أنه في الوقت الذي أقوم فيه بإنشاء AppContainer ، لم أقم بتشغيل PostgreSQLContainer بعد ، مما يعني أنه لا يمكنني الحصول على المنفذ المخصص منه ، وهناك حاجة لتكوين jdbcUrl .

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

كيفية حل هذه المشكلة؟ الطريقة الأولى التي أسميها "كسول".

 class MyTest extends FreeSpec with BeforeAndAfterAll { lazy val pgCont = PostgreSQLContainer() lazy val appCont = AppContainer(pgCont.jdbcUrl, pgCont.username, pgCont.password) override def beforeAll(): Unit = { super.beforeAll() pgCont.start() appCont.start() } override def afterAll(): Unit = { super.afterAll() appCont.stop() pgCont.stop() } // tests here } 

والفكرة الرئيسية هي إنشاء حاويات باستخدام فال كسول . بعد ذلك ، لن يتم تهيئتها على الفور في مُنشئ الاختبار ، ولكن سيتم انتظار المكالمة الأولى. سنقوم التهيئة في afterAll و afterAll ، والتي توفرها خاصية BeforeAndAfterAll من ScalaTest. في beforeAll تبدأ الحاويات ، afterAll ، يتم إيقاف تشغيلها. نظرًا لأن الحاويات معلنة كسولًا ، في الوقت الذي يتم فيه استدعاء طريقة البدء قبل كل شيء ، سيتم إنشاؤها وتهيئتها وبدء تشغيلها.

ومع ذلك ، لا يزال يحدث خطأ لا يمكنني الانضمام إلى المضيف المحلي: 32787:

 org.postgresql.util.PSQLException: Connection to localhost:32787 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections. 

يبدو أننا استخدمنا jdbcUrl ، لماذا يظهر المضيف المحلي؟ دعونا نرى كيف يعمل jdbcUrl:

 @Override public String getJdbcUrl() { return "jdbc:postgresql://" + getContainerIpAddress() + ":" + getMappedPort(POSTGRESQL_PORT) + "/" + databaseName; } 

انها مجرد سلسلة السلسلة. كل شيء واضح مع الثوابت ؛ لا يمكن كسرها. يجب أن يعمل getMappedPort أيضًا ، لأننا قمنا بالفعل getMappedPort . databaseName ثابت ثابت ترميز. ولكن مع getContainerIpAddress أكثر إثارة للاهتمام. بالاسم ، يمكننا أن نفترض أنه ينبغي أن يعود عنوان IP للحاوية. ولكن إذا قمت بتشغيل هذا الرمز ، فقد تبين أنه يقوم دائمًا بإرجاع المضيف المحلي. كما اتضح ، هذه الطريقة غير مخصصة للتفاعل بين الحاوية: getContainerIpAddress يوفر التفاعل من الاختبارات داخل الحاوية .

توصية مطور Testcontainers: إنشاء شبكة مخصصة للاتصال بين الحاويات . يعمل Docker-compose على شيء من هذا القبيل: فهو ينشئ شبكة ويحل كل شيء بمفرده.

لذلك تحتاج إلى إنشاء شبكة.

 class MyTest extends FreeSpec with BeforeAndAfterAll { val network: Network = Network.newNetwork() val dbName = "some_db" val pgContainerAlias = "postgres" val jdbcUrl = s"jdbc:postgresql://$pgContainerAlias:5432/$dbName" lazy val pgCont = { val c = PostgreSQLContainer("postgres:9.6") c.container.withNetwork(network) c.container.withNetworkAliases(pgContainerAlias) c.container.withDatabaseName(dbName) c } lazy val appCont = { val c = AppContainer(jdbcUrl, pgCont.username, pgCont.password) c.container.withNetwork(network) c } override def beforeAll(): Unit = { super.beforeAll() pgCont.start() appCont.start() } override def afterAll(): Unit = { super.afterAll() appCont.stop() pgCont.stop() network.close() } // tests here } 

الآن لدينا لتكوين jdbcUrl لدينا يدويا. نحتاج أيضًا إلى تمكين الحاويات لدينا في الشبكة ، وتعيين الاسم المستعار لـ PostgreSQLContainer بحيث يمكن الوصول إليه داخل الشبكة عن طريق بعض اسم المجال. في النهاية ، يجب أن تتذكر "قتل" الشبكة.

وأخيرا ، سوف يعمل مثل هذا البرنامج.

في الإصدارات الأخيرة من testcontainers-scala ، يتم دعم تهيئة الحاوية البطيئة خارج الصندوق:

 class MyTest extends FreeSpec with ForAllTestContainer with BeforeAndAfterAll { val network: Network = Network.newNetwork() val dbName = "some_db" val pgContainerAlias = "postgres" val jdbcUrl = s"jdbc:postgresql://$pgContainerAlias:5432/$dbName" lazy val pgCont = { val c = PostgreSQLContainer("postgres:9.6") c.container.withNetwork(network) c.container.withNetworkAliases(pgContainerAlias) c.container.withDatabaseName(dbName) c } lazy val appCont = { val c = AppContainer(jdbcUrl, pgCont.username, pgCont.password) c.container.withNetwork(network) c } override val container = MultipleContainers(pgCont, appCont) override def afterAll(): Unit = { super.afterAll() network.close() } // tests here } 

يمكنك استخدام ForAllTestContainer و ForAllTestContainer مرة أخرى. في beforeAll لم تعد بحاجة إلى beforeAll ترتيب البدء يدويًا. الآن يمكن لـ MultiContainers العمل باستخدام val lazy وتشغيلها بالترتيب الصحيح ، ولا تقوم بالتهيئة الصارمة فور الإنشاء. في الوقت نفسه ، يلزم أيضًا إجراء عمليات معالجة بالشبكة المخصصة و jdbcUrl يدويًا.

يسخر


ومع ذلك ، لا تزال هناك مشاكل. على سبيل المثال موكي. في بعض الأحيان ، ليس من المناسب جدًا إنشاء نوع من التبعية في حاوية عامل الميناء. نحن نستخدم Spark JobServer ، الذي ينشئ وظائف Spark ويتحكم في دورة حياته في Spark. نحن نستخدم طريقتين: "إنشاء" و "إعطاء حالة".

لتشغيل Spark JobServer داخل عامل ميناء. من الضروري رفع Spark ، وحتى وقت قريب ، لم يكن بها حاوية ترسية على الإطلاق وكان من الضروري تجميعها بنفسك. بالإضافة إلى ذلك ، يستخدم Spark JobServer PostgreSQL لتخزين الحالات. نتيجة لذلك ، يجب عليك القيام بالكثير من العمل الصعب عندما تحتاج حقًا إلى طريقتين فقط مع واجهة برمجة تطبيقات بسيطة.

ولكن يمكنك إلقاء نظرة خاطفة على تنفيذ Spark JobServer وإنشاء نموذج يتصرف بنفس الطريقة ، لكن لا يتطلب ذلك تبعيات Spark JobServer الأصلية.

يبدو مثل هذا (في المثال ، رمز زائف مبسط):

 val hostIp = ??? AppContainer(sparkJobServerMockHost = hostIp) val sparkJobServerMock = new SparkJobServerMock() sparkJobServerMock.init(someData) val apiResult = appApi.callMethod() assert(apiResult == someData) 

http- API Spark JobServer. - , . , , , mock.

- , . : «» config; , host.

SparkJobServerMock , host-, docker-, , , docker-.

? docker-, , gateway , docker-.

, Testcontainers API. , Testcontainers docker-java-, . «» docker-:

 val client: com.github.dockerjava.api.DockerClient = DockerClientFactory .instance .client val networkInfo: com.github.dockerjava.api.model.Network = client .inspectNetworkCmd() .withNetworkId(network.getId) .exec() val hostIp: String = networkInfo .getIpam .getConfig .get(0) .getGateway 

-, DockerClient . Testcontainers DockerClientFactory . c inspectNetworkCmd . , info, gateway.

, , .

— . Docker : Windows, Mac, . Linux. , , Linux .

, Testcontainers . , docker-. :

 Testcontainers.exposeHostPorts(sparkJobServerMockPort) 

, . docker-. `host.testcontainers.internal` .

, :

 val sparkJobServerMockHost = "host.testcontainers.internal" val sparkJobServerMockPort = 33333 Testcontainers.exposeHostPorts(sparkJobServerPort) AppContainer(sparkJobServerMockHost, sparkJobServerMockPort) 


Testcontainers


, , Testcontainers , . Java-, Scala-. :

  • . , testcontainers-java JUnit, testcontainers-scala ScalaTest, testcontainers-java . Scala- .
  • Scala . . , . , predefined Java-. , .
  • API . API, . , . , , .

النتائج


. Docker , , , , network gateway.

Testcontainers — , . API , .

Java-, . — . .

, docker-, .

— , , , . .?

, .

— - ?

Kubernetes, . end-to-end , , , , .

, , unit-, .

— Kubernetes ?

-, , -, , , , Spark Kubernetes ; , .

, , unit-, , , break point , , .

, , , CI , .

, minicube — Mac, . , , , , .

— ? : master? , - , , 2.1, 2.2, ?

ImageName, Postgres 9.6.

 val pgContainer: PostgreSQLContainer = PostgreSQLContainer("postgres:9.6") 

9.6, 10. [ ], .

Image tag — , — , . , latest .

— , ?

, CI , GitLab CI , , Branch Name.

— , , , ? - , ? 20- , ?

-, , . , , , , , .

- , , full-time , , , .

commit', , , , Android, iOS . . , , , , — .

, , -: - , - . , - .

تريد المزيد من التفاصيل حول الخدمات الصغيرة نفسها وليس فقط على Scala - يحتوي برنامج ScalaConf الخاص بنا على إجابات لأسئلة متنوعة. أكثر اهتمامًا بالعمارة والترابطات بين أجزائها المختلفة - تعال إلى HighLoad ++ في 7-8 نوفمبر.

كل شيء لذيذ للغاية ، وليس من الواضح ماذا تختار ، ثم الاشتراك في النشرة الإخبارية التي نتحدث فيها عن التقارير وجمع المواد المفيدة حول هذا الموضوع.

Source: https://habr.com/ru/post/ar464307/


All Articles