تطبيقات TDD على Spring Boot: صقل الاختبارات والعمل مع السياق

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


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


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


في البداية ، كما جرت العادة ، نظرية صغيرة. للأشخاص الذين ليسوا على دراية بمفاهيم bin ، والسياق ، والتكوين ، أوصي بمعرفة منعشة ، على سبيل المثال ، في مقالتي الجانب العكسي من Spring / Habr .


اختبار الربيع


Spring Test هي إحدى المكتبات المتضمنة في Spring Framework ، في الواقع ، كل ما هو موضح في قسم الوثائق حول اختبار التكامل يتعلق به. المهام الأربع الرئيسية التي تحلها المكتبة هي:


  • إدارة حاويات IoC الربيع والتخزين المؤقت بين الاختبارات
  • توفير حقن التبعية لفئات الاختبار
  • توفير إدارة المعاملات المناسبة لاختبارات التكامل
  • توفير مجموعة من الفئات الأساسية لمساعدة المطور على كتابة اختبارات التكامل

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


اختبار دورة الحياة



تبدو دورة حياة الاختبار كما يلي:


  1. SpringRunner ملحق إطار الاختبار ( SpringRunner لـ JUnit 4 و SpringExtension لـ JUnit 5) Test Context Bootstrapper
  2. ينشئ Boostrapper TestContext - الفئة الرئيسية التي تخزن الحالة الحالية للاختبار والتطبيق
  3. TestContext بإعداد TestContext مختلفة (مثل بدء المعاملات قبل الاختبار ثم العودة إلى الحالة السابقة) ، ويضخ التبعيات في فئات الاختبار (جميع حقول @Autowired في فئات الاختبار) ويقوم بإنشاء سياقات
  4. يتم إنشاء سياق باستخدام Context Loader - يأخذ التكوين الأساسي للتطبيق ويدمجه مع تكوين الاختبار (الخصائص المتراكبة ، الملفات الشخصية ، الصناديق ، أدوات التهيئة ، إلخ)
  5. يتم تخزين السياق مؤقتًا باستخدام مفتاح مركب يصف التطبيق بالكامل - مجموعة من الصناديق ، الخصائص ، إلخ.
  6. يعمل الاختبار

في الواقع ، يتم إجراء جميع الأعمال القاسية لإدارة الاختبارات ، عن طريق spring-test Spring Boot Test ، ويضيف Spring Boot Test بدوره ، عدة فئات مساعدة ، مثل @DataJpaTest و @SpringBootTest ، أدوات مساعدة مفيدة مثل TestPropertyValues لتغيير خصائص السياق ديناميكيًا. كما يسمح لك بتشغيل التطبيق @MockBean ويب حقيقي ، أو كبيئة وهمية (دون الوصول عبر HTTP) ، كما أنه مناسب لمسح مكونات النظام باستخدام @MockBean ، إلخ.

سياق التخزين المؤقت


ولعل أحد الموضوعات الغامضة للغاية في اختبار التكامل الذي يثير العديد من الأسئلة والمفاهيم الخاطئة هو التخزين المؤقت للسياق (انظر الفقرة 5 أعلاه) بين الاختبارات وتأثيرها على سرعة الاختبارات. تعليق متكرر أسمع أن اختبارات التكامل "بطيئة" و "تشغيل التطبيق لكل اختبار." لذلك ، فإنهم يركضون - لكن ليس لكل اختبار. سيتم إعادة استخدام كل سياق (مثل مثيل التطبيق) إلى الحد الأقصى ، أي إذا استخدمت 10 اختبارات نفس تكوين التطبيق ، فسيبدأ التطبيق مرة واحدة لجميع الاختبارات العشرة. ماذا يعني "نفس التكوين" للتطبيق؟ بالنسبة لـ Spring Test ، فهذا يعني أن مجموعة الفاصوليا وفئات التكوين والملفات التعريف والخصائص ، وما إلى ذلك ، لم تتغير. في الممارسة العملية ، يعني هذا ، على سبيل المثال ، أن هذين الاختبارين سيستخدمان نفس السياق:


 @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { } 

يقتصر عدد السياقات الموجودة في ذاكرة التخزين المؤقت على 32 - بالإضافة إلى مبدأ LRSU ، سيتم حذف أحدها من ذاكرة التخزين المؤقت.

ما الذي يمكن أن يمنع اختبار الربيع من إعادة استخدام السياق من ذاكرة التخزين المؤقت وإنشاء واحدة جديدة؟


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


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


 @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { @MockBean CakeFinder cakeFinderMock; } 

TestPropertySource
أي تغيير خاصية يغير تلقائيا مفتاح ذاكرة التخزين المؤقت ويتم إنشاء سياق جديد.


ActiveProfiles
سيؤثر تغيير ملفات التعريف النشطة أيضًا على ذاكرة التخزين المؤقت.


ContextConfiguration
وبالطبع ، فإن أي تغيير التكوين سيخلق سياق جديد.


نبدأ القاعدة


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


  1. تشغيل مرة واحدة قبل جميع الاختبارات في الصف.
  2. قم بتشغيل نسخة عشوائية وقاعدة بيانات منفصلة لكل سياق تم تخزينه في ذاكرة التخزين المؤقت (يحتمل أن يكون أكثر من فئة واحدة).

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


لا يرتبط الخيار الأول بالربيع ، بل يرتبط بإطار اختبار. على سبيل المثال ، يمكنك إنشاء ملحق لـ JUnit 5 .

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


واجهة ApplicationContextInitializer هي المسؤولة عن تنفيذ الإجراءات مع السياق قبل البدء في Spring.


ApplicationContextInitializer


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


في حالتي ، يبدو الفصل كالتالي:


 public class EmbeddedPostgresInitializer implements ApplicationContextInitializer<GenericApplicationContext> { @Override public void initialize(GenericApplicationContext applicationContext) { EmbeddedPostgres postgres = new EmbeddedPostgres(); try { String url = postgres.start(); TestPropertyValues values = TestPropertyValues.of( "spring.test.database.replace=none", "spring.datasource.url=" + url, "spring.datasource.driver-class-name=org.postgresql.Driver", "spring.jpa.hibernate.ddl-auto=create"); values.applyTo(applicationContext); applicationContext.registerBean(EmbeddedPostgres.class, () -> postgres, beanDefinition -> beanDefinition.setDestroyMethodName("stop")); } catch (IOException e) { throw new RuntimeException(e); } } } 

أول ما يحدث هنا هو إطلاق Postgres المضمن من المكتبة المدمجة في yandex-qatools / postgresql . بعد ذلك ، يتم إنشاء مجموعة من الخصائص - عنوان URL لـ JDBC للقاعدة التي تم إطلاقها حديثًا ونوع برنامج التشغيل وسلوك الإسبات للنظام (الإنشاء تلقائيًا). شيء واحد غير واضح هو spring.test.database.replace=none - هذا ما نقوله DataJpaTest لعدم محاولة الاتصال بقاعدة البيانات المضمنة ، مثل H2 ، وليس لاستبدال حاوية مصدر البيانات (يعمل هذا).


والنقطة المهمة الأخرى هي application.registerBean(…) . بشكل عام ، لا يمكن بالطبع تسجيل هذه الحبة - إذا لم يقم أحد باستخدامها في التطبيق ، فلن تكون هناك حاجة خاصة إليها. يلزم التسجيل فقط لتحديد طريقة التدمير التي سيتصل بها Spring عند إتلاف السياق ، وفي هذه الحالة ، postgres.stop() هذه الطريقة postgres.stop() وتوقف قاعدة البيانات.


بشكل عام ، هذا كل شيء ، انتهى السحر ، إن وجد. الآن سأقوم بتسجيل المُهيئ في سياق اختبار:


 @DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) ... 

أو حتى للراحة ، يمكنك إنشاء التعليق التوضيحي الخاص بك ، لأننا نحب جميعًا التعليقات التوضيحية!


 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) public @interface EmbeddedPostgresTest { } 

الآن أي اختبار مشروح بواسطة @EmbeddedPostgrestTest سيبدأ قاعدة البيانات على منفذ عشوائي وباسم عشوائي ، قم بتكوين Spring للاتصال بقاعدة البيانات هذه وإيقافه في نهاية الاختبار.


 @EmbeddedPostgresTest class JpaCakeFinderTestWithEmbeddedPostgres { ... } 

استنتاج


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


روابط لمقالات أخرى في السلسلة


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


All Articles