مقدمة: الخطأ الذي يدفع المطورين للجنون
\n\nهل سبق لك أن كتبت اختبارًا بلغة Kotlin لتطبيق Android، وأضفت ActivityTestRule، ثم شغّلت الاختبار وفجأة ظهرت لك هذه الرسالة المقلقة: "The @Rule must be public"؟ إذا كان الأمر كذلك، فأنت لست وحدك. هذا السؤال على Stack Overflow حصد آلاف المشاهدات، وكل يوم يقع العشرات من المطورين في نفس المشكلة.
المشكلة تبدو بسيطة، لكنها في Kotlin تتحول إلى رحلة بحث حقيقية. في Java، كل شيء يعمل بسلاسة: @Rule، حقل عام (public) — والأمر منتهٍ. أما في Kotlin... حسنًا، أنت تعرف القصة. اليوم سنفهم لماذا يحدث هذا، والأهم من ذلك، كيف نصلحه نهائيًا.
بالمناسبة، إذا كنت تفضل Python، فموقع PythonLib لديه الكثير من المحتوى المفيد عن الاختبارات. لكن اليوم، سنتحدث عن Kotlin الخالص وAndroid.
\n\nلماذا يظهر خطأ "The @Rule must be public"؟
\n\nلنتذكر كيف يعمل ActivityTestRule في JUnit. هذه القاعدة (Rule) تقوم بتشغيل Activity قبل كل اختبار وتنهيه بشكل صحيح بعده. في Java، تكتب:
@Rule\npublic ActivityTestRule<MainActivity> activityTestRule =\n new ActivityTestRule<>(MainActivity.class);\n\n\nالأمر بسيط: الحقل عام (public)، والقاعدة عامة. في Kotlin، كل شيء يكون public افتراضيًا، لكن هناك خدعة. عندما تكتب:
\n\n@Rule\nval activityTestRule = ActivityTestRule(MainActivity::class.java)\n\n\n...يقوم Kotlin بإنشاء حقل خاص (private) ودالة getter عامة. لكن JUnit يتطلب حقلًا عامًا وليس دالة getter. هذا هو السر كله.
\n\nيظهر الخطأ "The @Rule must be public" تحديدًا لأن JUnit لا يرى الحقل الخاص بك — فهو خاص (private).
الحل رقم 1: استخدام @get:Rule (الأكثر شيوعًا)
\n\nأسهل وأشهر طريقة هي تطبيق التعليق التوضيحي (annotation) على دالة getter بدلاً من الحقل. لهذا، يوجد في Kotlin الصيغة @get:Rule:
class MainActivityTest {\n\n @get:Rule\n val activityTestRule = ActivityTestRule(MainActivity::class.java)\n\n @Test\n fun testActivityIsNotNull() {\n assertNotNull(activityTestRule.activity)\n }\n}\n\n\nماذا يحدث هنا؟ نقول: "قم بتعليق دالة getter، وليس الحقل". JUnit يرى دالة getter العامة، ويحصل على القاعدة، وكل شيء يعمل. هذا الحل مجرب من قبل مئات المطورين.
\n\nهذا الحل هو "خطتك أ". إنه موجز، واضح، ولا يتطلب مكتبات إضافية.
\n\nالحل رقم 2: جعل الحقل عامًا باستخدام @JvmField
\n\nإذا كنت لا تحب صيغة @get:Rule (على الرغم من أنها ممتازة)، يمكنك استخدام التعليق التوضيحي @JvmField. هذا يجبر Kotlin على إنشاء حقل عام بدون دالة getter:
class MainActivityTest {\n\n @JvmField\n @Rule\n val activityTestRule = ActivityTestRule(MainActivity::class.java)\n\n @Test\n fun testActivityIsNotNull() {\n assertNotNull(activityTestRule.activity)\n }\n}\n\n\nيعمل تمامًا مثل كود Java. العيب: كود أكثر قليلاً. الميزة: توضيح صريح أن الحقل يجب أن يكون عامًا.
\n\nشخصيًا، أفضل @get:Rule — فهو أقصر ويُقرأ بشكل طبيعي أكثر لمطور Kotlin. لكن الخيار لك.
الحل رقم 3: استخدام ActivityScenarioRule (الطريقة الحديثة)
\n\nوالآن دعنا نتحدث عما توصي به Google بدلاً من ActivityTestRule. في مكتبة androidx.test.ext:junit يوجد ActivityScenarioRule. إنه أكثر مرونة ويتكامل بشكل أفضل مع اختبارات Android الحديثة.
مع هذه القاعدة، نفس القصة — تحتاج إلى استخدام @get:Rule أو @JvmField:
class MainActivityTest {\n\n @get:Rule\n val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)\n\n @Test\n fun testActivityIsNotNull() {\n activityScenarioRule.scenario.onActivity { activity ->\n