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

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

أي ، في البداية كان لدينا نوع من المهمة ، نكتب رمزًا لحلها ، ويفترض أن يكسبنا المال. إذا لم يعمل هذا الرمز لسبب ما بشكل صحيح ، فإنه يحل المهمة الخاطئة ويكسبنا المال الخاطئ. الأعمال لا تحب هذا النوع من المال - تبدو سيئة في البيانات المالية.
لذلك ، بالنسبة إلى رمزنا المهم ، لدينا اختبارات:

عادة هناك. الآن ، على الأرجح ، كل شخص لديه هذا. تتحقق الاختبارات من أن الرمز يحل المشكلة الصحيحة ويجني المال المناسب. لكن الخدمة لا تقتصر على الرمز ، وبجانب الرمز هناك أيضًا تكوين:

على الأقل في جميع المشاريع التي شاركت فيها تقريبًا ، كان هذا التكوين ، بشكل أو بآخر. (يمكنني أن أتذكر بضع حالات فقط من سنوات واجهة المستخدم الأولى ، حيث لم تكن هناك ملفات تهيئة ، ولكن تم تكوين كل شيء من خلال واجهة المستخدم) في هذا التكوين ، توجد منافذ وعناوين ومعلمات خوارزمية.
ما أهمية التهيئة للاختبار؟
هذه هي الحيلة: أخطاء في تنفيذ برنامج الضرر لا تقل عن أخطاء في التعليمات البرمجية. يمكنهم أيضًا أن يتسببوا في تنفيذ التعليمات البرمجية لمهمة خاطئة - وانظر أعلاه.
والعثور على أخطاء في التهيئة أكثر صعوبة مما كانت عليه في التعليمات البرمجية ، نظرًا لأن التكوين لا يتم تجميعه عادةً. لقد استشهدت بملفات الخصائص كمثال ، بشكل عام هناك خيارات مختلفة (JSON ، XML ، يخزنها شخص ما في YAML) ، ولكن من المهم ألا يتم التحقق من أي من هذا المترجمات ، وبالتالي ، لا يتم التحقق منه. إذا قمت بإغلاق ملف Java دون قصد - على الأرجح ، فلن يمر ببساطة التجميع. خطأ إملائي عشوائي في الملكية لن يثير أي شخص ، بل سيذهب إلى العمل.
ولا يبرز IDE الخطأ في التكوين أيضًا ، لأنه يعرف فقط الأكثر بدائية حول تنسيق (على سبيل المثال) ملفات الخصائص: أنه يجب أن يكون هناك مفتاح وقيمة ، و "يساوي" ، نقطتان أو مسافة بينهما. ولكن حقيقة أن القيمة يجب أن تكون رقمًا أو منفذ شبكة أو عنوانًا - لا يعرف IDE أي شيء.
وحتى إذا اختبرت التطبيق في UAT أو في بيئة التدريج ، فإن هذا لا يضمن أيضًا أي شيء. لأن التكوين ، كقاعدة عامة ، يختلف في كل بيئة ، وفي اختبار UAT قمت باختبار تكوين UAT فقط.
دقة أخرى هي أنه حتى في الإنتاج ، لا تظهر أخطاء التكوين أحيانًا على الفور. قد لا تبدأ الخدمة على الإطلاق - وهذا سيناريو جيد. ولكن يمكن أن تبدأ ، وتعمل لفترة طويلة جدًا - حتى اللحظة X ، حيث سيكون من الضروري بالضبط المعلمة التي حدث فيها الخطأ. وهنا تجد أن الخدمة التي لم تتغير كثيرًا مؤخرًا توقفت عن العمل فجأة.
بعد كل ما قلته - يبدو أن اختبار التكوينات يجب أن يكون موضوعًا ساخنًا. ولكن من الناحية العملية ، يبدو الأمر مثل هذا:

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

الوضع هو هذا: لدينا خدمات n على نفس المضيف ، كل واحد منهم يرفع خادم JMX الخاص به على المنفذ الخاص به ، ويصدر بعض أجهزة المراقبة JMX. يتم تكوين المنافذ لجميع الخدمات في الملف. لكن الملف يشغل عدة صفحات ، وهناك العديد من الخصائص الأخرى - غالبًا ما يتبين أن منافذ الخدمات المختلفة تتعارض. من السهل ارتكاب خطأ. ثم كل شيء تافه: بعض الخدمات لا ترتفع ، بعد أن لا ترتفع لمن يعتمدون عليها - المختبرون غاضبون.
يتم حل هذه المشكلة في عدة أسطر. هذا الاختبار ، (الذي يبدو لي) هو أول اختبار لنا ، بدا كالتالي:

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

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

المثال التالي هو نفسه تقريبًا ، ولكن تم تعديله قليلاً - لقد أزلت كل "jmx". هذه المرة نتحقق من جميع الخصائص التي تسمى شيء هناك منفذ. يجب أن تكون قيمًا صحيحة وأن تكون منفذ شبكة صالحًا. يقوم Matcher validNetworkPort () بإخفاء أداة مطابقة المطابقة المخصصة لدينا ، والتي تتحقق من أن القيمة أعلى من نطاق منافذ النظام ، وأقل من نطاق المنافذ المؤقتة ، حسنًا ، نحن نعلم أن بعض المنافذ الموجودة على خوادمنا مشغولة مسبقًا - إليك القائمة الكاملة منها مخفية أيضًا في هذا هو المطابق.
لا يزال هذا الاختبار بدائيًا جدًا. لاحظ أنه لا يوجد ما يشير إلى الخاصية المحددة التي نتحقق منها - فهي ضخمة. اختبار واحد من هذا النوع يمكنه التحقق من 500 خاصية باسم "... port" ، والتحقق من أنها كلها أعداد صحيحة في النطاق المطلوب ، مع جميع الشروط اللازمة. بمجرد أن يكتبوا ، عشرات الخطوط - وهذا كل شيء. هذه ميزة مريحة للغاية ، تظهر لأن التكوين يحتوي على تنسيق بسيط: عمودين ومفتاح وقيمة. لذلك ، يمكن معالجتها بكميات كبيرة.
مثال اختباري آخر. ماذا نتحقق هنا؟

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

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

وهي مكتوبة لـ SQL * Plus. SQL * Plus هي أداة من الستينيات ، وتتطلب جميع أنواع الأشياء الغريبة: على سبيل المثال ، للتأكد من أن نهاية الملف في سطر جديد. بالطبع ، ينسى الناس بانتظام وضع نهاية الخط هناك ، لأنهم لم يولدوا في الستينيات.

ومرة أخرى يتم حلها بنفس الخطوط العشرة: نختار جميع ملفات SQL ، ونتحقق من وجود شرطة مائلة في النهاية. بسيطة ومريحة وسريعة.
مثال آخر على "مثل ملف نصي" هو crontabs. تبدأ خدمات crontab التي نقدمها وتتوقف. غالبًا ما تسبب خطأين:

أولاً ، تنسيق تعبير الجدول الزمني. الأمر ليس معقدًا ، ولكن لا أحد يتفقده قبل الإطلاق ، لذا من السهل وضع مساحة إضافية ، وفاصلة ، وما شابه.
ثانيًا ، كما في المثال السابق ، يجب أن تكون نهاية الملف أيضًا في سطر جديد.
وكل هذا سهل التحقق. نهاية الملف مفهومة ، ولكن للتحقق من الجدول ، يمكنك العثور على مكتبات جاهزة تقوم بتحليل تعبير cron. قبل التقرير ، غوغل: كان هناك ستة منهم على الأقل. لقد وجدت ستة ، ولكن بشكل عام قد يكون هناك المزيد. عندما كتبنا ، أخذنا أبسط ما تم العثور عليه ، لأننا لم نكن بحاجة إلى التحقق من محتويات التعبير ، ولكن فقط صحته النحوية ، بحيث قام cron بتحميله بنجاح.
من حيث المبدأ ، يمكنك إنهاء المزيد من الفحوصات - تأكد من أنك تبدأ في اليوم المناسب من الأسبوع ، وأنك لا توقف الخدمات في منتصف يوم العمل. لكن تبين أن ذلك لم يكن مفيدًا جدًا لنا ، ولم نتعب أنفسنا.
فكرة أخرى تعمل بشكل جيد هي البرامج النصية shell. بالطبع ، الكتابة في Java محلل كامل لنصوص bash هي متعة للشجعان. لكن خلاصة القول هي أن عددًا كبيرًا من هذه النصوص ليست باش كاملة. نعم ، هناك نصوص باش حيث تكون الشفرة مباشرة ، والجحيم والجحيم ، حيث تسقط مرة واحدة في السنة ، وتؤدي اليمين ، وتهرب. لكن العديد من مخطوطات باش هي نفس التكوينات. هناك عدد من متغيرات النظام ومتغيرات البيئة التي تم تعيينها على القيمة المطلوبة ، وبالتالي تكوين البرامج النصية الأخرى التي تستخدم هذه المتغيرات. ومن السهل فهم هذه المتغيرات من ملف bash هذا والتحقق من شيء عنها.

على سبيل المثال ، تحقق من تثبيت JAVA_HOME على كل بيئة ، أو أن بعض مكتبات jni التي نستخدمها موجودة في LD_LIBRARY_PATH. بطريقة ما ، انتقلنا من إصدار Java إلى آخر ، وقمنا بتوسيع الاختبار: تحققنا من أن JAVA_HOME يحتوي على "1.8" في تلك المجموعة الفرعية من البيئة ، والتي قمنا بنقلها تدريجيًا إلى الإصدار الجديد.
إليك بعض الأمثلة. دعني ألخص الجزء الأول من الاستنتاجات:
- اختبارات التكوين مربكة في البداية ، هناك حاجز نفسي. ولكن بعد التغلب عليه ، هناك العديد من الأماكن في التطبيق التي لا تغطيها الشيكات ويمكن تغطيتها.
- ثم تتم كتابتها بسهولة وبهجة : هناك الكثير من "الفواكه المنخفضة المعلقة" التي تقدم فوائد عظيمة بسرعة).
- تقليل تكلفة اكتشاف أخطاء التكوين وتصحيحها. نظرًا لأن هذه هي في الواقع اختبارات وحدة ، يمكنك تشغيلها على جهاز الكمبيوتر الخاص بك ، حتى قبل الالتزام - وهذا يقلل بشكل كبير من حلقة الملاحظات. بالطبع ، كان سيتم اختبار العديد منها في مرحلة نشر الاختبار ، على سبيل المثال. ولن يتم اختبار الكثير - إذا كان هذا تكوين إنتاج. وبالتالي يتم فحصها مباشرة على الكمبيوتر المحلي.
- يعطون الشباب الثاني. بمعنى أن هناك شعور أنه لا يزال بإمكانك اختبار الكثير من الأشياء المثيرة للاهتمام. في الواقع ، في الكود لم يعد من السهل العثور على ما يمكنك اختباره.
الجزء 2. حالات أكثر تعقيدا
دعنا ننتقل إلى اختبارات أكثر تعقيدًا. بعد تغطية معظم الشيكات التافهة ، مثل تلك المبينة هنا ، يطرح السؤال: هل من الممكن التحقق من شيء أكثر تعقيدًا؟
ماذا يعني "أصعب"؟ الاختبارات التي وصفتها للتو لها البنية التالية تقريبًا:

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

بالطبع ، يمكنك القيام بذلك بالضبط: قم بتحميل واحد ، والثاني ، واسحب شيء ما في مكان ما ، وقم بلصقه في رمز الاختبار. ولكن يمكنك أن تتخيل مدى ضخامة الكود ومدى قراءته. بدأنا من هذا ، ولكن بعد ذلك أدركنا مدى صعوبة ذلك. كيف تقوم بعمل أفضل؟
إذا كنت تحلم ، سيكون الأمر أكثر ملاءمة ، ثم حلمت أن الاختبار سيبدو كما لو كنت أشرح ذلك بلغة بشرية:
@Theory public void eachEnvironmentIsXXX( Environment environment ) { for( Server server : environment.servers() ) { for( Service service : server.services() ) { Properties config = buildConfigFor( environment, server, service );
لكل بيئة ، يتم استيفاء الشرط. للتحقق من ذلك ، تحتاج من البيئة للعثور على قائمة بالخوادم وقائمة الخدمات. ثم قم بتحميل التكوينات وتحقق من شيء عند التقاطع. وبناءً على ذلك ، أحتاج إلى شيء من هذا القبيل ، أطلقت عليه اسم تخطيط النشر.

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

لأغراضنا ، تعد منطقة الخادم مهمة ، ومركز البيانات المحدد ، من حيث المبدأ ، أيضًا ، بحيث يكون النسخ الاحتياطي / الأساسي في مراكز بيانات مختلفة. هذه كلها خصائص إضافية للخادم ، وهي خاصة بالمشروع ، ولكن على الشريحة فهي قاسم مشترك.
أين يمكن الحصول على تخطيط النشر؟ يبدو أنه في أي شركة كبيرة يوجد نظام إدارة البنية التحتية ، كل شيء موصوف هناك ، إنه موثوق وموثوق وكل ذلك ... في الواقع لا.
على الأقل ، أظهرت ممارستي في مشروعين أنه من الأسهل استخدام الكود الصلب أولاً ، وبعد ذلك ، بعد ثلاث سنوات ... ترك بشرة قاسية.
نحن نعيش مع هذا المشروع لمدة ثلاث سنوات حتى الآن. في الثانية ، على ما يبدو ، ما زلنا نندمج مع إدارة البنية التحتية في عام واحد ، ولكن كل هذه السنوات عشنا مثل هذا. من الخبرة ، من المنطقي تأجيل مهمة التكامل مع الرسائل الفورية من أجل الحصول على اختبارات جاهزة في أقرب وقت ممكن ، مما سيظهر أنها تعمل ومفيدة. ومن ثم قد يتبين أن هذا التكامل قد لا يكون ضروريًا للغاية ، لأن توزيع الخدمات عبر الخوادم لا يتغير كثيرًا.
يمكن أن يكون الرمز الصلب حرفياً مثل هذا:
public enum Environment { PROD( PROD_UK_PRIMARY, PROD_UK_BACKUP, PROD_US_PRIMARY, PROD_US_BACKUP, PROD_SG_PRIMARY, PROD_SG_BACKUP ) … public Server[] servers() {…} } public enum Server { PROD_UK_PRIMARY(“rflx-ldn-1"), PROD_UK_BACKUP("rflx-ldn-2"), PROD_US_PRIMARY(“rflx-nyc-1"), PROD_US_BACKUP("rflx-nyc-2"), PROD_SG_PRIMARY(“rflx-sng-1"), PROD_SG_BACKUP("rflx-sng-2"), public Service[] services() {…} }
أسهل طريقة نستخدمها في مشروعنا الأول هي تعداد البيئة بقائمة من الخوادم في كل منها. هناك قائمة بالخوادم ، ويبدو أنه يجب أن تكون هناك قائمة بالخدمات ، لكننا خدعنا: لدينا نصوص برمجية (والتي هي أيضًا جزء من التكوين).

يديرون خدمات لكل بيئة. وأسلوب الخدمات () ببساطة grep'a جميع الخدمات من ملف الخادم الخاص به. يتم ذلك بسبب عدم وجود العديد من البيئات ، ونادرًا ما تتم إضافة أو حذف الخوادم - ولكن هناك العديد من الخدمات ، ويتم تبديلها في كثير من الأحيان. كان من المنطقي تحميل التخطيط الفعلي للخدمات من النصوص البرمجية حتى لا يتم تغيير التخطيط الثابت في كثير من الأحيان.
بعد إنشاء نموذج تكوين البرنامج هذا ، تظهر مكافآت ممتعة. على سبيل المثال ، يمكنك كتابة اختبار مثل هذا:

الاختبار هو أن جميع الخدمات الرئيسية موجودة في كل بيئة. لنفترض أن هناك أربع خدمات رئيسية ، والباقي قد يكون أو لا يكون ، ولكن بدون هذه الخدمات الأربعة لا معنى له. يمكنك التحقق من أنك لم تنساهم في أي مكان ، وأنهم جميعًا لديهم نسخ احتياطية في نفس البيئة. في معظم الأحيان ، تحدث مثل هذه الأخطاء عند تكوين UAT لهذه الحالات ، ولكن يمكن أيضًا أن تتسرب إلى PROD. في النهاية ، تضيع الأخطاء في UAT الوقت وأعصاب المختبرين.
السؤال الذي يطرح نفسه هو الحفاظ على أهمية نموذج التكوين. يمكنك أيضًا كتابة اختبار لهذا.
public class HardCodedLayoutConsistencyTest { @Theory eachHardCodedEnvironmentHasConfigFiles(Environment env){ … } @Theory eachConfigFileHasHardCodedEnvironment(File configFile){ … } }
هناك ملفات التكوين ، وهناك تخطيط نشر في التعليمات البرمجية. ويمكنك التحقق من ذلك لكل بيئة / خادم / إلخ. هناك ملف التكوين المقابل ، ولكل ملف بالتنسيق المطلوب - البيئة المقابلة. بمجرد نسيان إضافة شيء إلى مكان واحد ، سوف يسقط الاختبار.
خلاصة القول هي تخطيط النشر:
- يبسط كتابة اختبارات معقدة تجمع التكوينات من أجزاء مختلفة من التطبيق.
- يجعلها أكثر وضوحا وأكثر قابلية للقراءة. إنهم ينظرون إلى الطريقة التي تفكر بها بها على مستوى عالٍ ، وليس الطريقة التي يمرون بها من خلال التكوينات.
- أثناء إنشائه ، عندما يطرح الناس أسئلة ، يتبين الكثير من الأشياء المثيرة للاهتمام حول النشر. القيود ، المعرفة المقدسة الضمنية ، تأتي ، على سبيل المثال ، فيما يتعلق بإمكانية استضافة بيئتين على خادم واحد. اتضح أن المطورين يفكرون بشكل مختلف ويكتبون خدماتهم وفقًا لذلك. ومثل هذه اللحظات مفيدة لتسوية بين المطورين.
- حسنا يكمل الوثائق (خاصة إذا لم يكن كذلك). حتى لو كان هناك ، من الرائع بالنسبة لي ، كمطور ، أن أرى هذا في الكود. علاوة على ذلك ، يمكنك كتابة تعليقات مهمة بالنسبة لي ، وليس لشخص آخر. ويمكنك أيضا رمز ثابت. بمعنى ، إذا قررت أنه لا يمكن أن تكون هناك بيئتان على نفس الخادم ، يمكنك إدراج شيك ، ولن يتم ذلك الآن. على الأقل ستكتشف إذا حاول شخص ما. أي أن هذا توثيق له القدرة على تطبيقه. هذا مفيد جدا
دعنا ننتقل. بعد كتابة الاختبارات ، "استقروا" لمدة عام ، بدأ بعضهم في السقوط. يبدأ البعض في الانخفاض في وقت سابق ، لكنه ليس مخيفًا جدًا. إنه أمر مخيف عندما يقع اختبار مكتوب قبل عام ، تنظر إلى رسالة الخطأ ، ولا تفهم.

لنفترض أنني أفهم وأوافق على أن هذا منفذ شبكة غير صالح - ولكن أين هو؟ قبل الحديث ، نظرت إلى حقيقة أن لدينا 1200 ملف ملكية في المشروع ، متناثرة على 90 وحدة ، بإجمالي 24000 سطر فيها. (على الرغم من أنني فوجئت ، ولكن إذا كنت تحسب ، فهذا ليس مثل هذا العدد الكبير - لخدمة واحدة لأربعة ملفات.) أين هذا المنفذ؟
من الواضح أن assertThat () له وسيطة رسالة ، يمكنك إدخال شيء فيه سيساعد في تحديد المكان. ولكن عندما تكتب اختبارًا ، لا تفكر فيه. وحتى إذا كنت تعتقد ، لا يزال عليك تخمين الوصف الذي سيتم تفصيله بما فيه الكفاية لفهمه خلال عام. أود أتمتة هذه اللحظة ، حتى تكون هناك طريقة لكتابة الاختبارات باستخدام الإنشاء التلقائي لوصف أكثر أو أقل وضوحًا ، يمكنك من خلاله العثور على خطأ.
مرة أخرى ، حلمت وحلمت بشيء من هذا القبيل:
SELECT environment, server, component, configLocation, propertyName, propertyValue FROM configuration(environment, server, component) WHERE propertyName like “%.port%” and propertyValue is not validNetworkPort()
هذا مثل SQL الزائف - حسنًا ، أنا أعرف SQL فقط ، وقد ألقى الدماغ بالحل خارج ما هو مألوف. الفكرة هي أن معظم اختبارات التكوين تتكون من عدة قطع من نفس النوع. أولاً ، يتم تحديد مجموعة فرعية من المعلمات حسب الشرط:

ثم ، فيما يتعلق بهذه المجموعة الفرعية ، نتحقق من شيء ما فيما يتعلق بالقيمة:

وبعد ذلك ، إذا كانت هناك خصائص لا تفي قيمها بالرغبة ، فهذه هي "الورقة" التي نريد تلقيها في رسالة الخطأ:

في وقت من الأوقات ، فكرت حتى إذا كان بإمكاني كتابة محلل يشبه SQL ، لأنه الآن ليس صعبًا. ولكنني أدركت بعد ذلك أن IDE لن تدعمه واقترحه ، لذا سيتعين على الناس الكتابة بشكل أعمى على "SQL" التي تم إنشاؤها ذاتيًا ، بدون مطالبات IDE ، بدون تجميع ، دون التحقق - وهذا ليس مناسبًا جدًا. لذلك ، كان عليّ البحث عن حلول مدعومة بلغة البرمجة الخاصة بنا. إذا كان لدينا .NET ، فإن LINQ سيساعد ، فهو يشبه SQL تقريبًا.
لا يوجد LINQ في Java ، أقرب ما يكون هو التدفقات. هذه هي الطريقة التي يجب أن يظهر بها هذا الاختبار في التدفقات:
ValueWithContext[] incorrectPorts = flattenedProperties( environment ) .filter( propertyNameContains( ".port" ) ) .filter( !isInteger( propertyValue ) || !isValidNetworkPort( propertyValue ) ) .toArray(); assertThat( incorrectPorts, emptyArray() );
يأخذ flattenedProperties () جميع تكوينات هذه البيئة ، وجميع الملفات لجميع الخوادم والخدمات ويوسعها إلى جدول كبير. هذا في الأساس جدول يشبه SQL ، ولكن في شكل مجموعة من كائنات Java. و flattenedProperties () ترجع هذه المجموعة من السلاسل كتدفق.

ثم أضفت بعض الشروط على هذه المجموعة من كائنات Java. في هذا المثال: نختار تلك التي تحتوي على "port" في propertyName ونقوم بتصفية تلك التي لا يتم فيها تحويل القيم إلى Integer ، أو لا يتم تحويلها من النطاق الصالح. هذه قيم خاطئة ، ومن الناحية النظرية ، يجب أن تكون مجموعة فارغة.

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

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

هذا مثال ، هناك المزيد. التباديل التعشيش الثلاثي ، ويتم استخدامه طوال التكوين:

هناك عدد قليل من الملفات في التكوين نفسه ، ولكن يتم تضمينها في بعضها البعض. يستخدم ملحق صغير لخصائص iu - Apache Commons Configuration ، والذي يدعم فقط التضمين والأذونات في الأقواس.
وقد قام المؤلف بعمل رائع باستخدام هذين الشيئين فقط. أعتقد أنه بنى عليها آلة تورينج هناك. في بعض الأماكن ، يبدو أنه يحاول بالفعل إجراء حسابات باستخدام الادراج والبدائل. لا أعرف ما إذا كان نظام تورينج هذا كاملاً ، لكنه ، في رأيي ، حاول أن يثبت أن هذا صحيح.
وغادر الرجل. كتب التطبيق يعمل وغادر البنك. كل شيء يعمل ، لا أحد يفهم التكوين بشكل كامل.
إذا أخذنا خدمة منفصلة ، فعندئذٍ يتحول إلى 10 شوائب ، إلى عمق ثلاثي ، وبشكل إجمالي ، إذا تم توسيع كل شيء ، 450 معلمة. في الواقع ، تستخدم هذه الخدمة المحددة 10-15٪ منها ، أما بقية المعلمات فهي لخدمات أخرى ، لأن الملفات مشتركة ، وتستخدمها عدة خدمات. ولكن ما يستخدمه 10-15٪ بالضبط من هذه الخدمة المعينة ليس من السهل فهمه. يبدو أن المؤلف يفهم. شخص ذكي جدا جدا.
كانت المهمة ، على التوالي ، هي تبسيط التكوين وإعادة هيكلته. في الوقت نفسه ، أردت إبقاء التطبيق يعمل ، لأنه في هذه الحالة تكون فرص ذلك منخفضة. اريد ان:
- تبسيط التكوين.
- لذلك ، بعد إعادة الهيكلة ، لا تزال كل خدمة تحتوي على جميع معلماتها الضرورية.
- بحيث ليس لديه معلمات إضافية. 85٪ من الأشخاص غير المرتبطين بها لا يجب أن يزعجوا الصفحة.
- هذه الخدمات لا تزال مرتبطة بنجاح في مجموعات وتنفيذ التعاون.
تكمن المشكلة في أنه من غير المعروف مدى جودة اتصالهم الآن ، لأن النظام فائض للغاية. على سبيل المثال ، التطلع إلى المستقبل: أثناء إعادة البناء ، اتضح أنه في أحد تكوينات الإنتاج يجب أن يكون هناك أربعة خوادم في مقطع النسخ الاحتياطي ، ولكن في الواقع كان هناك خادمان. نظرًا لارتفاع مستوى التكرار ، لم يلاحظ أحد ذلك - ظهر الخطأ عن طريق الخطأ ، ولكن في الواقع كان مستوى التكرار أقل لفترة طويلة مما توقعنا. النقطة هي أنه لا يمكننا الاعتماد على حقيقة أن التكوين الحالي صحيح في كل مكان.
أفضي إلى حقيقة أنه لا يمكنك مقارنة التكوين الجديد بالتهيئة القديمة فقط. قد يكون متكافئًا ، ولكن يظل في نفس الوقت خاطئًا في مكان ما. من الضروري التحقق من المحتوى المنطقي.
الحد الأدنى من البرنامج: عزل كل معلمة منفصلة لكل خدمة يحتاجها والتحقق من صحتها ، أن هذا المنفذ هو منفذ ، والعنوان هو عنوان ، TTL هو رقم موجب ، إلخ. وتحقق من العلاقات الرئيسية التي ترتبط بها الخدمات بشكل أساسي عند نقاط النهاية الرئيسية. أردت تحقيق ذلك على الأقل. هذا ، على عكس الأمثلة السابقة ، المهمة هنا ليست التحقق من المعلمات الفردية ، ولكن لتغطية التكوين بأكمله بشبكة كاملة من الشيكات.
كيفية اختباره؟
public class SimpleComponent { … public void configure( final Configuration conf ) { int port = conf.getInt( "Port", -1 ); if( port < 0 ) throw new ConfigurationException(); String ip = conf.getString( "Address", null ); if( ip == null ) throw new ConfigurationException(); … } … }
كيف حللت هذه المشكلة؟ هناك بعض المكونات البسيطة ، في المثال يتم تبسيطه إلى أقصى حد. (بالنسبة لأولئك الذين لم يعثروا على تكوين Apache Commons: فإن كائن التكوين يشبه الخصائص ، إلا أنه لا يزال لديه الأساليب المكتوبة getInt () ، getLong () ، وما إلى ذلك ؛ يمكننا افتراض أن هذه هي خصائص على المنشطات الصغيرة.) افترض أن المكون يحتاج إلى معلمتين: على سبيل المثال ، عنوان TCP ومنفذ TCP. نحن سحبهم والتحقق. ما هي الأجزاء الأربعة المشتركة هنا؟

هذا هو اسم المعلمة ، والنوع ، والقيم الافتراضية (هنا تافهة: فارغة و -1 ، وأحيانًا توجد قيم عاقل) وبعض عمليات التحقق. تم التحقق من صحة المنفذ هنا ببساطة شديدة وغير كاملة - يمكنك تحديد المنفذ الذي سيمر عبره ، ولكنه لن يكون منفذ شبكة صالحًا. لذلك ، أود تحسين هذه اللحظة أيضًا. لكن قبل كل شيء ، أريد تحويل هذه الأشياء الأربعة إلى شيء واحد. على سبيل المثال ، هذا:
IProperty<Integer> PORT_PROPERTY = intProperty( "Port" ) .withDefaultValue( -1 ) .matchedWith( validNetworkPort() ); IProperty<String> ADDRESS_PROPERTY = stringProperty( "Address" ) .withDefaultValue( null ) .matchedWith( validIPAddress() );
مثل هذا الكائن المركب هو وصف لخاصية تعرف اسمها ، والقيمة الافتراضية ، يمكنها إجراء التحقق (هنا أستخدم مُطابق هامكريست مرة أخرى). وهذا الكائن يحتوي على شيء مثل هذه الواجهة:
interface IProperty<T> { FetchedValue<T> fetch( final Configuration config ) } class FetchedValue<T> { public final String propertyName; public final T propertyValue; … }
أي أنه بعد إنشاء كائن محدد لتطبيق معين ، يمكنك أن تطلب منه استخراج المعلمة التي يمثلها من التكوين. وسوف يقوم بسحب هذه المعلمة ، والتحقق من العملية ، إذا لم تكن هناك معلمة ، فسيعطي قيمة افتراضية ، ويؤدي إلى النوع المطلوب ، ويعيدها على الفور بالاسم.
هذا هو ، هنا اسم المعلمة وقيمة فعلية ستراها الخدمة إذا طلبت من هذا التكوين. هذا يسمح لك بلف عدة أسطر من التعليمات البرمجية في كيان واحد ، وهذا هو التبسيط الأول الذي سأحتاجه.
التبسيط الثاني الذي أحتاجه لحل المشكلة هو تقديم مكون يحتاج إلى العديد من الخصائص لتكوينه. نموذج تكوين المكون:

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

خدعة هذا الرأي هي أن النماذج قابلة للدمج. إذا كان هناك مكون يستخدم مكونات أخرى ، وتم دمجها هناك ، فبنفس الطريقة يمكن لنموذج هذا المكون المعقد دمج نتائج مكالمات مكونين فرعيين.
أي أنه من الممكن بناء تسلسل هرمي لنماذج التكوين موازية لتسلسل المكونات نفسها. في النموذج العلوي ، استدعاء fetch () ، والذي سيعيد الورقة من المعلمات التي سحبها من التكوين بأسمائها - بالضبط تلك التي سيحتاجها المكون المقابل في الوقت الفعلي. إذا كتبنا جميع النماذج بشكل صحيح ، بالطبع.
أي أن المهمة هي كتابة مثل هذه النماذج لكل مكون في التطبيق لديه حق الوصول إلى التكوين. في تطبيقي ، كان هناك عدد قليل جدًا من هذه المكونات: التطبيق نفسه مورق تمامًا ، ولكنه يعيد استخدام الرمز بنشاط ، لذلك يتم تكوين 70 فئة رئيسية فقط. بالنسبة لهم ، كان علي أن أكتب 70 نموذجًا.
كم تكلف:
- 12 خدمات
- 70 فئة قابلة للتكوين
- => 70 ConfigurationModels (~ 60 تافهة) ؛
- 1-2 أسابيع.
لقد قمت ببساطة بفتح الشاشة باستخدام رمز المكون الذي يقوم بتكوين نفسه ، وفي الشاشة التالية كتبت الرمز الخاص بـ ConfigurationModel المقابل. معظمها تافه ، مثل المثال الموضح. في بعض الحالات ، هناك فروع وتحولات مشروطة - يصبح الرمز أكثر تفرعاً ، ولكن يتم حل كل شيء أيضًا. في أسبوع ونصف إلى أسبوعين حللت هذه المشكلة ، لجميع المكونات 70 وصفت النماذج.
ونتيجة لذلك ، عندما نجمعها معًا ، نحصل على الكود التالي:

لكل خدمة / بيئة / إلخ. نأخذ نموذج التكوين ، أي العقدة العلوية لهذه الشجرة ، ونطلب الحصول على كل شيء من التكوين. عند هذه النقطة ، تمر جميع عمليات التحقق من الداخل ، كل من الخصائص ، عندما تسحب نفسها خارج التكوين ، تتحقق من صحتها. إذا لم يمر واحد على الأقل ، سيخرج استثناء. يتم الحصول على جميع التعليمات البرمجية عن طريق التحقق من صحة جميع القيم في عزلة.
ترابط الخدمة
لا يزال لدينا سؤال حول كيفية التحقق من الاعتماد المتبادل للخدمات. هذا أكثر تعقيدًا بعض الشيء ، تحتاج إلى النظر في نوع الاعتماد المتبادل. اتضح لي أن الترابط يتلخص في حقيقة أن الخدمات يجب أن "تلتقي" في نقاط نهاية الشبكة. يجب أن تستمع الخدمة A إلى العنوان الذي ترسل إليه الخدمة B الحزم ، والعكس صحيح. في المثال الخاص بي ، جاءت جميع التبعيات بين تكوينات الخدمات المختلفة إلى هذا. كان من الممكن حل هذه المشكلة بطريقة مباشرة: الحصول على المنافذ والعناوين من الخدمات المختلفة والتحقق منها. سيكون هناك العديد من الاختبارات ، ستكون ضخمة. أنا شخص كسول ولم أرد هذا. لذلك ، فعلت خلاف ذلك.
أولاً ، كنت أرغب في تلخيص نقطة نهاية هذه الشبكة بطريقة أو بأخرى. على سبيل المثال ، لاتصال TCP تحتاج إلى معلمتين فقط: العنوان والمنفذ. للاتصال المتعدد ، أربع معلمات. أود طيها إلى شيء ما. فعلت هذا في كائن نقطة النهاية ، والذي يخفي كل شيء تحتاجه في الداخل. تعد الشريحة مثالاً على OutcomingTCPEndpoint ، وهو اتصال شبكة TCP صادر.
IProperty<IEndpoint> TCP_REQUEST = outcomingTCP(
Endpoint matches(), Endpoint, , .
« »? , : , , - , — . , , / . , , .
, , ---, , Endpoint. ConfigurationModels — , . ? :
ValueWithContext[] allEndpoints = flattenedConfigurationValues(environment) .filter( valueIsEndpoint() ) .toArray(); ValueWithContext[] unpairedEndpoints = Arrays.stream( allEndpoints ) .filter( e -> !hasMatchedEndpoint(e, allEndpoints) ) .toArray(); assertThat( unpairedEndpoints, emptyArray() );
environment' endpoint', , , , . . « » O(n^2), , endpoint' , .
Endpoint , , . , , - .
, , , «» — , . . , , . , .
. , , . , , , c, , .
ConfigurationModel :
, . , , , — . : , . , , , , .
. , ConfigurationModels, . , UDP- , , .
, endpoints , .dot. . — .
. الاستنتاجات:
Heisenbug 2018 Piter , : 6-7 Heisenbug . . 1 — .