اختبار رمز متعدد مؤشرات الترابط وغير متزامن

تحية! كانت المهمة هذا الأسبوع هي كتابة اختبار تكامل لتطبيق Spring Boot باستخدام تفاعل غير متزامن مع الأنظمة الخارجية. تحديث الكثير من المواد حول تصحيح التعليمات البرمجية ذات مؤشرات الترابط المتعددة. إن مقال "اختبار الشفرة المتعددة الخيوط وغير المتزامنة" للمخرج جوناثان هالترمان ، وترجمتي له أدناه ، جذبت الانتباه.

بفضل shalomman ، شرودر و FTOH على تعليقات كود الأكثر أهمية من المقال الأصلي.

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

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

نذهب بالتوازي


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

messageBus.registerHandler(message - > { System.out.println("Received " + message); }); messageBus.publish("test"); 

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

 String msg = "test"; messageBus.registerHandler(message -> { System.out.println("Received " + message); assertEquals(message, msg); }; messageBus.publish(msg); 

يبدو أفضل. نجري اختبارنا وهو أخضر. باردة! لكن الرسالة المستلمة لم تطبع في أي مكان ، كان هناك خطأ ما في مكان ما.

انتظر ثانية


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

 String msg = "test"; messageBus.registerHandler(message -> { System.out.println("Received " + message); assertEquals(message, msg); }; messageBus.publish(msg); Thread.sleep(1000); 

اختبارنا أخضر والتعبير المستلم مطبوع كما هو متوقع. باردة! لكن ثانية واحدة من النوم تعني أن اختبارنا يتم لمدة ثانية واحدة على الأقل ، ولا يوجد شيء جيد فيه. يمكننا تقليل وقت النوم ، ولكن بعد ذلك نواجه خطر إتمام الاختبار قبل تلقي رسالة. نحتاج إلى طريقة للتنسيق بين سلسلة رسائل الاختبار الرئيسية ومؤشر رسالة المعالج. بالنظر إلى الحزمة java.util.concurrent ، من المؤكد أننا سنجد ما يمكننا استخدامه. ماذا عن CountDownLatch ؟

 String msg = "test"; CountDownLatch latch = new CountDownLatch(1); messageBus.registerHandler(message -> { System.out.println("Received " + message); assertEquals(message, msg); latch.countDown(); }; messageBus.publish(msg); latch.await(); 

في هذا النهج ، نشارك CountDownLatch بين مؤشر ترابط الاختبار الرئيسي ومؤشر ترابط معالج الرسالة. يتم فرض الخيط الرئيسي على الانتظار على مانع. مؤشر الترابط اختبار إصدارات مؤشر الترابط الرئيسي المعلقة عن طريق استدعاء countDown () على مانع بعد تلقي الرسالة. لم نعد بحاجة إلى النوم لثانية واحدة. يستغرق اختبارنا نفس الوقت الذي تحتاجه بالضبط.

سعيد جدا؟


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

 latch.await(1, TimeUnit.SECONDS); 

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

 messageBus.registerHandler(message -> { assertEquals(message, msg); latch.countDown(); }; 

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

 CountDownLatch latch = new CountDownLatch(1); new Thread(() -> { assertTrue(false); latch.countDown(); }).start(); latch.await(); 

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

 CountDownLatch latch = new CountDownLatch(1); AtomicReference<AssertionError> failure = new AtomicReference<>(); new Thread(() -> { try { assertTrue(false); } catch (AssertionError e) { failure.set(e); } latch.countDown(); }).start(); latch.await(); if (failure.get() != null) throw failure.get(); 

بداية سريعة ونعم ، فشل الاختبار ، كما ينبغي! الآن يمكننا العودة وإضافة CountDownLatches ، حاول / catch و AtomicReference كتل لجميع حالات الاختبار لدينا. باردة! في الواقع ، ليست باردة ، يبدو مثل الغلاية.

قطع القمامة


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

 String msg = "test"; Waiter waiter = new Waiter(); messageBus.registerHandler(message -> { waiter.assertEquals(message, msg); waiter.resume(); }; messageBus.publish(msg); waiter.await(1, TimeUnit.SECONDS); 

في هذا الاختبار ، نرى أن Waiter قد حل محل CountDownLatch و AtomicReference الخاص بنا. مع Waiter ، نقوم بحظر مؤشر الترابط الرئيسي للاختبار ، ونجري الاختبار ، ثم نستأنف مؤشر الترابط الرئيسي للاختبار حتى يمكن إكمال الاختبار. إذا فشلت عملية الفحص ، فحينئذٍ ستقوم الدعوة إلى waiter.await بإطلاق القفل تلقائيًا ورمي أي عطل ، مما يؤدي إلى اجتياز الاختبار أو إخفاقه ، كما ينبغي ، حتى لو تم إجراء الفحص من خيط آخر.

حتى أكثر توازنا


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

 Waiter waiter = new Waiter(); messageBus.registerHandler(message -> { waiter.resume(); }; messageBus.publish("one"); messageBus.publish("two"); waiter.await(1, TimeUnit.SECONDS, 2); 

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

ملخص


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

ترجم بواسطة middle_java

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


All Articles