مقدمة: "لماذا يفشل اختباري بينما يجب أن ينجح؟"
\n\nهل سبق لك أن كتبت اختبار وحدة كان من المفترض أن يلتقط استثناءً، ولكنه بدلاً من ذلك فشل بخطأ أحمر؟ مألوف؟ سؤال "How do you assert that a certain exception is thrown in JUnit tests?" على Stack Overflow حصد آلاف المشاهدات ومئات التعليقات. وهذا ليس من قبيل الصدفة: اختبار الاستثناءات هو أحد تلك المواضيع التي حتى المطورين ذوي الخبرة يتعثرون فيها أحيانًا.
\n\nفي عام 2025، عندما أصبحت جودة الكود عاملاً حاسمًا (خاصة في الخدمات المصغرة وخطوط أنابيب AI)، فإن القدرة على التحقق من الاستثناءات بشكل صحيح ليست مجرد "أخلاق حميدة"، بل كفاءة أساسية. في هذه المقالة، سنحلل جميع الطرق الحالية لتأكيد الاستثناءات في JUnit 5، ونعرض المخاطر الخفية ونقدم قائمة مرجعية لأفضل الممارسات. هيا بنا!
\n\nلماذا من المهم اختبار الاستثناءات؟
\n\nتخيل: أنت تكتب طريقة divide(int a, int b). إذا كان b == 0، يجب أن ترمي الطريقة ArithmeticException. بدون اختبار لهذا السلوك، فإنك تخاطر أنه بعد شهر، مطور آخر (أو أنت نفسك) قد يزيل التحقق عن طريق الخطأ، وسيتعطل التطبيق في الإنتاج بخطأ غير مفهوم.
اختبارات الاستثناءات هي شبكة الأمان الخاصة بك. إنها توثق السلوك المتوقع للنظام في الحالات الحدودية. و JUnit 5 يوفر عدة أدوات أنيقة لهذا الغرض.
\n\nالطريقة 1: assertThrows - ملك الاختبار الحديث
\n\nبدءًا من JUnit 5 (وحزمة org.junit.jupiter.api)، أصبحت طريقة assertThrows المعيار الفعلي. لماذا؟ لأنها موجزة وقابلة للقراءة ووظيفية.
import static org.junit.jupiter.api.Assertions.assertThrows;\n\n@Test\nvoid whenDivideByZero_thenArithmeticException() {\n assertThrows(ArithmeticException.class, \n () -> calculator.divide(10, 0));\n}\n\n\nماذا يحدث هنا؟ assertThrows تأخذ فئة الاستثناء المتوقع ولامدا مع الكود الذي يجب أن يرمي الاستثناء. إذا تم رمي الاستثناء - يكون الاختبار أخضر. إذا لم يتم - يكون أحمر. كل شيء بسيط وواضح.
نسخة موسعة: التحقق من رسالة الاستثناء
\n\nأحيانًا لا يكفي مجرد التحقق من نوع الاستثناء - تحتاج إلى التأكد من أن الرسالة تحتوي على النص الصحيح. assertThrows تُرجع كائن الاستثناء، ويمكنك التحقق من خصائصه:
import static org.junit.jupiter.api.Assertions.*;\n\n@Test\nvoid whenInvalidEmail_thenExceptionWithMessage() {\n IllegalArgumentException exception = assertThrows(\n IllegalArgumentException.class,\n () -> userService.register("invalid-email")\n );\n \n assertTrue(exception.getMessage().contains("Некорректный email"));\n}\n\n\nهذا النهج مفيد بشكل خاص عند اختبار الاستثناءات المخصصة ذات الحقول (مثل رمز الخطأ).
\n\nالطريقة 2: expected القديمة الجيدة (إرث JUnit 4)
\n\nإذا كنت تعمل مع JUnit 4 أو تدعم كودًا قديمًا، فمن المحتمل أنك صادفت السمة expected في التعليق التوضيحي @Test:
@Test(expected = ArithmeticException.class)\npublic void whenDivideByZero_thenException() {\n calculator.divide(10, 0);\n}\n\n\nالعيب: هذه الطريقة لا تسمح بالتحقق من رسالة الاستثناء أو خصائصه. بالإضافة إلى ذلك، إذا تم رمي الاستثناء في وقت أبكر مما تتوقع (على سبيل المثال، في سطر التهيئة)، فقد ينجح الاختبار بشكل خاطئ. في عام 2025، نوصي بتجنب هذا النهج في الكود الجديد.
\n\nالطريقة 3: Try-catch مع علامة (لعشاق التحكم)
\n\nقبل ظهور JUnit 5، استخدم الكثيرون بناء try-catch مع علامة منطقية. يبدو ضخمًا، لكنه يعطي تحكمًا كاملاً:
@Test\nvoid whenDivideByZero_thenExceptionCaught() {\n boolean thrown = false;\n try {\n calculator.divide(10, 0);\n } catch (Arithmeti