تطوير الجوّال

اختبارات UI Jetpack Compose: أتمتة الشاشات خطوة بخطوة

5 دقائق للقراءة

🔝 الدليل الرئيسي: Kotlin وJetpack Compose في 2026

مشروع Compose يكبر بدون اختبارات تلقائية يبدأ في التراجع بسرعة. تعديل في composable يكسر شاشة أخرى بصمت، الفريق يفقد الثقة، وكل release يصبح جلسة اختبارات يدوية مرهقة. طبقة اختبار Jetpack Compose الرسمية تُسمى androidx.compose.ui.test وتكشف API معبّر لقيادة شجرة composables في اختبار instrumenté أو Robolectric. مع اختبار وحدوي JVM لـ ViewModel واختبار DAO لـ Room، تُشكّل هرمًا كاملًا.

المتطلبات

  • مشروع Compose بشاشة وظيفية واحدة على الأقل (راجع أول شاشات Compose)
  • ViewModel وStateFlow في مكانه
  • محاكي Android 16 (API 36) مُعَدّ، أو Robolectric مُثبَّت
  • الوقت المُقدَّر: 90 دقيقة

الخطوة 1 — فهم طوابق الاختبار الثلاثة

تطبيق مُختبَر جيدًا له ثلاثة مستويات. اختبارات وحدوية JVM (سريعة، بدون Android) تُغطّي ViewModels وRepositories وأصناف الأعمال — حوالي 70% من التغطية. اختبارات تكامل (Robolectric أو محاكي) تُغطّي Room وDataStore والتسلسل — حوالي 20%. اختبارات UI Compose تُغطّي مسارات المستخدم الحرجة — حوالي 10%. هذا الهرم يُحقّق توازنًا بين زمن التنفيذ والثقة المُكتسبة.

الخطوة 2 — إضافة تبعيات الاختبار

[versions]
junit = "4.13.2"
androidxJunit = "1.3.0"
espressoCore = "3.7.0"
composeBom = "2026.05.00"

[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
dependencies {
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.compose.ui.test.junit4)
    debugImplementation(libs.androidx.compose.ui.test.manifest)
}

الخطوة 3 — كتابة أول اختبار Compose

@RunWith(AndroidJUnit4::class)
class WelcomeCardTest {

    @get:Rule val composeTestRule = createComposeRule()

    @Test
    fun afficherTitreEtDescription() {
        composeTestRule.setContent {
            HelloKotlinTheme {
                WelcomeCard(
                    title = "Mon titre",
                    description = "Ma description"
                )
            }
        }

        composeTestRule.onNodeWithText("Mon titre").assertIsDisplayed()
        composeTestRule.onNodeWithText("Ma description").assertIsDisplayed()
    }
}

createComposeRule() يُنشئ بيئة Compose معزولة دون Activity كاملة. setContent { ... } مكافئ Activity حقيقية. onNodeWithText("...") يختار العقدة. assertIsDisplayed() يتحقق من العرض. إذا لم يجد المُحدّد شيئًا، يفشل الاختبار برسالة واضحة.

الخطوة 4 — اختبار تفاعلات المستخدم

@Test
fun clickerArticleAppelleCallback() {
    var idClique = -1
    composeTestRule.setContent {
        HelloKotlinTheme {
            ListeArticles(
                articles = listOf(
                    Article(id = 42, titre = "Titre 42", contenu = "...")
                ),
                onArticleClick = { idClique = it.id }
            )
        }
    }

    composeTestRule.onNodeWithText("Titre 42").performClick()
    assertEquals(42, idClique)
}

تقنية spy بمتغيّر محلي تعمل للاختبارات البسيطة. للحالات المعقدة، نستخدم ViewModel مزيف أو callback object يُسجّل الاستدعاءات. ميزة lambda المباشرة: تبقى قابلة للقراءة ولا تعتمد على mock library.

الخطوة 5 — اختبار شاشة مع ViewModel

@Test
fun afficheSpinnerPendantChargement() {
    val fakeRepo = FakeArticlesRepository(delaiInfini = true)
    val viewModel = ListeArticlesViewModel(fakeRepo)

    composeTestRule.setContent {
        HelloKotlinTheme {
            EcranListeArticles(viewModel = viewModel)
        }
    }

    composeTestRule.onNodeWithTag("loading").assertIsDisplayed()
}

@Test
fun afficheErreurAvecBoutonReessayer() = runTest {
    val fakeRepo = FakeArticlesRepository(erreur = "Connexion impossible")
    val viewModel = ListeArticlesViewModel(fakeRepo)

    composeTestRule.setContent {
        HelloKotlinTheme {
            EcranListeArticles(viewModel = viewModel)
        }
    }

    composeTestRule.onNodeWithText("Connexion impossible", substring = true).assertIsDisplayed()
    composeTestRule.onNodeWithText("Réessayer").performClick()
}

نستخدم onNodeWithTag للاختيار عبر Modifier.testTag("loading") — عملي لـ composables بدون نص ثابت مثل CircularProgressIndicator. الـ testTag يجب أن تبقى نادرة، محصورة على العقد التي نحتاج لاستهدافها في اختبار.

الخطوة 6 — اختبار مسار متعدد الشاشات مع التنقّل

@RunWith(AndroidJUnit4::class)
class ParcoursNavigationTest {

    @get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Test
    fun ouvrirArticleAffichePageDetail() {
        composeTestRule.onNodeWithText("Premier article").performClick()
        composeTestRule.onNodeWithText("Contenu du premier article").assertIsDisplayed()
        composeTestRule.onNodeWithContentDescription("Retour").assertIsDisplayed()
    }
}

لهذه الاختبارات التكاملية، نُعدّ MainActivity للاختبار تحقن Repository مزيف عبر Hilt (@HiltAndroidTest + HiltAndroidRule). يتجنّب الاعتماد على خادم حقيقي ويضمن بيانات حتمية.

الخطوة 7 — التنفيذ والتكامل في CI

الاختبارات instrumentés تُطلق عبر ./gradlew connectedDebugAndroidTest أو من Android Studio. على Mac M2، المحاكي يبدأ في 20-30 ثانية، تنفيذ اختبار يستغرق 500 ms إلى ثانيتين. لـ 50 اختبارًا، احسب 3-5 دقائق إجمالًا.

name: Tests UI Compose
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-java@v5
        with: { distribution: temurin, java-version: 17 }
      - uses: gradle/actions/setup-gradle@v4
      - run: ./gradlew test connectedDebugAndroidTest --no-daemon

على runner Linux مجاني، المحاكي غير متوفر أصلًا، لكن reactivecircus/android-emulator-runner يُعدّه. لـ 90% من المشاريع، Robolectric في CI يُغطّي الأساسي.

الخطوة 8 — التقاط لقطات مرجعية (golden tests)

@Test
fun rendu_carte_article_correspond_a_la_reference() {
    composeTestRule.setContent {
        HelloKotlinTheme {
            WelcomeCard(title = "Titre", description = "Description")
        }
    }

    composeTestRule.onRoot().captureToImage()
        .assertAgainstGolden("welcome_card_default")
}

عدة مكتبات تُنفّذ هذا النمط: Paparazzi (Cash App، الأشهر، JVM بحت، لا محاكي)، Roborazzi (مبني على Robolectric)، وShot. Paparazzi عمومًا الخيار الأفضل لـ Compose في 2026: تجميع فوري، مقارنة pixel-perfect، تكامل CI مباشر.

أخطاء شائعة

العَرَض السبب الحل
الاختبار يفشل بـ « ComposeNode not found » composable لم يُرَّكب بعد أضف composeTestRule.awaitIdle() أو انتظر عبر waitUntil
اختبار متذبذب مع الرسوم المتحركة Compose ينتظر نهاية الرسوم عطّل الرسوم في الاختبار
مُحدّد غير فريد عدة عقد بنفس النص استخدم useUnmergedTree = true أو testTag
الاختبار بطيء البدء إعادة تجميع Gradle أطلق عدة اختبارات في نفس الصنف
Hilt لا يحقن في اختبار غياب @HiltAndroidTest وHiltAndroidRule أضف التعليقات + module اختبار

توسيع التغطية دون إثقال

الخطأ الكلاسيكي: استهداف 100% تغطية وقضاء وقت أكبر في كتابة اختبارات من كتابة الكود. الأفضل تغطية 60% من المسارات الحرجة من 95% من كل شيء. عمليًا، لتطبيق نموذجي: اختبار وحدوي لكل ViewModel (نجاح، خطأ، حالة فارغة)، اختبار DAO لكل استعلام معقد، اختبار UI لكل شاشة كبرى، اختبار مسار لتدفقات حرجة (login، شراء، تقديم نموذج). 20-40 اختبار لتطبيق 8 شاشات.

لقياس التغطية، JaCoCo يتكامل عبر plugin Gradle kover (JetBrains). يُولّد تقرير HTML يُشير إلى الأسطر غير المُختبَرة. هدف معقول: 70% على طبقات الأعمال، 50% على طبقة UI Compose.

الأسئلة الشائعة

Espresso أم Compose Test؟
Compose Test لاختبار Compose، Espresso مفيد فقط إذا بقيت Views XML.

Robolectric أم محاكي؟
Robolectric لـ CI سريع وللتطوير. محاكي للتحقق في ظروف حقيقية قبل release.

كيف نختبر الرسوم المتحركة؟
عطّل عبر SnapshotStateObserver أو استخدم composeTestRule.mainClock.autoAdvance = false لقيادة الزمن يدويًا.

هل نختبر كل composable؟
لا. اختبر composables stateful والشاشات الكاملة. الصغار stateless لا يُقدّمون قيمة اختبار.

كيف نختبر AlertDialog؟
الـ dialogue يُرَّكب في نافذة منفصلة. استخدم onNodeWithText على الزر الذي يُطلقه، ثم onNodeWithText على زر dialogue.

اختبار instrumenté أم وحدوي؟
وحدوي JVM متى أمكن: أسرع 10×. instrumenté فقط عند الحاجة لبيئة Android كاملة.

Paparazzi أم Roborazzi؟
Paparazzi لـ Compose منصة أحادية. Roborazzi إذا كان المشروع يستخدم Robolectric.

كيف نتجنّب الاختبارات المتذبذبة؟
عطّل الرسوم، استخدم waitForIdle() قبل assertions، وفضّل waitUntil على Thread.sleep.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité