يتم استخدام اختبار واجهات المستخدم أو الشاشات للتحقق من السلوك الصحيح لرمز Compose الخاص بك، وتحسين جودة التطبيق عن طريق اكتشاف الأخطاء في وقت مبكر من عملية التطوير.
يوفر Compose مجموعة من واجهات برمجة التطبيقات التجريبية للعثور على العناصر والتحقق من سماتها وتنفيذ إجراءات المستخدم. وتشمل أيضًا ميزات متقدمة مثل معالجة الوقت.
علم المعاني
تستخدم اختبارات واجهة المستخدم في Compose الدلالات الدلالية للتفاعل مع التسلسل الهرمي لواجهة المستخدم. دلالات، كما يوحي الاسم، تعطي معنى لجزء من واجهة المستخدم. في هذا السياق، يمكن أن يعني "جزء من واجهة المستخدم" (أو عنصر) أي شيء بدءًا من عنصر واحد قابل للتعديل إلى ملء الشاشة. يتم إنشاء شجرة دلالات الألفاظ إلى جانب التسلسل الهرمي لواجهة المستخدم، وهي تصفها.
الشكل 1. هيكلية نموذجية لواجهة المستخدم وشجرة دلالاتها.
يُستخدم إطار عمل الدلالات في المقام الأول لإمكانية الوصول، لذلك تستفيد الاختبارات من المعلومات التي تعرضها دلالات الألفاظ حول التسلسل الهرمي لواجهة المستخدم. يقرّر المطوّرون المحتوى المعروض ومقدار المحتوى الذي يريدون عرضه.
الشكل 2. زر عادي يحتوي على رمز ونص.
على سبيل المثال، إذا كان هناك زر كهذا يتكون من رمز وعنصر نصي، فإن شجرة الدلالات الافتراضية تحتوي فقط على التسمية النصية "أعجبني". وذلك لأنّ بعض المواد، مثل Text
، تعرض بعض الخصائص لشجرة دلالات الألفاظ. يمكنك إضافة سمات إلى الشجرة الدلالية باستخدام
Modifier
.
MyButton(
modifier = Modifier.semantics { contentDescription = "Add to favorites" }
)
ضبط إعدادات
يصف هذا القسم كيفية إعداد الوحدة للسماح لك باختبار رمز الإنشاء.
أولاً، أضِف التبعيات التالية إلى ملف build.gradle
في الوحدة التي تحتوي على اختبارات واجهة المستخدم:
// Test rules and transitive dependencies:
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
// Needed for createAndroidComposeRule, but not createComposeRule:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
تتضمّن هذه الوحدة ComposeTestRule
وتطبيقًا لنظام التشغيل Android يسمى AndroidComposeTestRule
. من خلال هذه القاعدة، يمكنك ضبط محتوى "إنشاء"
أو الوصول إلى النشاط. يبدو اختبار واجهة المستخدم النموذجي لتطبيق Compose كما يلي:
// file: app/src/androidTest/java/com/package/MyComposeTest.kt
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
// use createAndroidComposeRule<YourActivity>() if you need access to
// an activity
@Test
fun myTest() {
// Start the app
composeTestRule.setContent {
MyAppTheme {
MainScreen(uiState = fakeUiState, /*...*/)
}
}
composeTestRule.onNodeWithText("Continue").performClick()
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}
اختبار واجهات برمجة التطبيقات
هناك ثلاث طرق رئيسية للتفاعل مع العناصر:
- تتيح لك أداة البحث اختيار عنصر واحد أو عدة عناصر (أو عقد في شجرة دلالات الألفاظ) لتأكيد هذه العناصر أو تنفيذ إجراءات بشأنها.
- يتم استخدام تأكيدات للتحقق من وجود العناصر أو أنها تتضمن سمات معينة.
- تؤدي الإجراءات إلى إدخال أحداث محاكاة للمستخدم على العناصر، مثل النقرات أو الإيماءات الأخرى.
تقبل بعض واجهات برمجة التطبيقات هذه العلامة SemanticsMatcher
للإشارة إلى عقدة واحدة أو أكثر في شجرة دلالات الدلالات.
الباحثون
يمكنك استخدام onNode
وonAllNodes
لاختيار عقدة واحدة أو أكثر على التوالي،
ولكن يمكنك أيضًا استخدام أدوات البحث المناسبة لعمليات البحث الأكثر شيوعًا، مثل
onNodeWithText
وonNodeWithContentDescription
وما إلى ذلك.
يمكنك تصفّح القائمة الكاملة في ورقة الملاحظات الموجزة لاختبار Compose.
اختيار عقدة واحدة
composeTestRule.onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
// Example
composeTestRule
.onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")
اختيار عُقد متعددة
composeTestRule
.onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
// Example
composeTestRule
.onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")
استخدام الشجرة غير المدمجة
تدمج بعض العُقد معلومات دلالات العناصر الثانوية. على سبيل المثال، يدمج زر يحتوي على عنصرين نصيين تسمياتهما:
MyButton {
Text("Hello")
Text("World")
}
من خلال الاختبار، يمكننا استخدام printToLog()
لعرض شجرة الدلالات:
composeTestRule.onRoot().printToLog("TAG")
يطبع هذا الرمز الإخراج التالي:
Node #1 at (...)px
|-Node #2 at (...)px
Role = 'Button'
Text = '[Hello, World]'
Actions = [OnClick, GetTextLayoutResult]
MergeDescendants = 'true'
إذا كنت بحاجة إلى مطابقة عقدة ضِمن الشجرة غير المدمجة، يمكنك ضبط السمة
useUnmergedTree
على القيمة true
:
composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")
يطبع هذا الرمز الإخراج التالي:
Node #1 at (...)px
|-Node #2 at (...)px
OnClick = '...'
MergeDescendants = 'true'
|-Node #3 at (...)px
| Text = '[Hello]'
|-Node #5 at (83.0, 86.0, 191.0, 135.0)px
Text = '[World]'
تتوفّر المَعلمة useUnmergedTree
في جميع أدوات البحث. على سبيل المثال، يُستخدم
هنا في أداة البحث عن onNodeWithText
.
composeTestRule
.onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()
التأكيدات
تحقق من التأكيدات من خلال استدعاء assert()
على SemanticsNodeInteraction
التي تم إرجاعها بواسطة الباحث مع واحد أو أكثر من المطابقات:
// Single matcher:
composeTestRule
.onNode(matcher)
.assert(hasText("Button")) // hasText is a SemanticsMatcher
// Multiple matchers can use and / or
composeTestRule
.onNode(matcher).assert(hasText("Button") or hasText("Button2"))
يمكنك أيضًا استخدام الدوال الملائمة للتأكيدات الأكثر شيوعًا، مثل
assertExists
وassertIsDisplayed
وassertTextEquals
وما إلى ذلك. يمكنك تصفّح القائمة الكاملة في ورقة الملاحظات الموجزة عن إنشاء الاختبار.
هناك أيضًا دوال للتحقّق من صحة التأكيدات في مجموعة من العُقد:
// Check number of matched nodes
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
// At least one matches
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())
المهام
لإدخال إجراء في عقدة، استدعِ الدالة perform…()
:
composeTestRule.onNode(...).performClick()
إليك بعض الأمثلة على الإجراءات:
performClick(),
performSemanticsAction(key),
performKeyPress(keyEvent),
performGesture { swipeLeft() }
يمكنك تصفح القائمة الكاملة في ورقة الملاحظات الموجزة لإنشاء اختبار.
ألعاب المطابقة
يصف هذا القسم بعض المُطابقات المتاحة لاختبار رمز الإنشاء.
أدوات المطابقة الهرمية
تتيح لك المتطابقات الهرمية صعود شجرة دلالات الألفاظ أو نزولها وإجراء مطابقة بسيطة.
fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher): SemanticsMatcher
في ما يلي بعض الأمثلة على أدوات المطابقة التي يتم استخدامها:
composeTestRule.onNode(hasParent(hasText("Button")))
.assertIsDisplayed()
أدوات الاختيار
هناك طريقة بديلة لإنشاء اختبارات وهي استخدام أدوات الاختيار التي تجعل بعض الاختبارات أكثر سهولة في القراءة.
composeTestRule.onNode(hasTestTag("Players"))
.onChildren()
.filter(hasClickAction())
.assertCountEquals(4)
.onFirst()
.assert(hasText("John"))
يمكنك تصفح القائمة الكاملة في ورقة الملاحظات الموجزة لإنشاء اختبار.
المزامنة
تتم مزامنة اختبارات الإنشاء مع واجهة المستخدم بشكل تلقائي. عند طلب تأكيد أو إجراء من خلال ComposeTestRule
، ستتم مزامنة الاختبار
مسبقًا، بالانتظار إلى أن تصبح شجرة واجهة المستخدم غير نشطة.
في العادة، لا يتعين عليك اتخاذ أي إجراء. ومع ذلك، هناك بعض الحالات الهامشية التي يجب أن تعرف عنها.
عندما تتم مزامنة الاختبار، يتقدم تطبيق Compose بفترة زمنية باستخدام ساعة افتراضية. وهذا يعني أن اختبارات Compose لا تعمل في الوقت الفعلي، وبالتالي يمكن اجتيازها في أسرع وقت ممكن.
ومع ذلك، إذا كنت لا تستخدم الطرق التي تزامن اختباراتك، لن تتم إعادة تركيبها وستظهر واجهة المستخدم مؤقتًا.
@Test
fun counterTest() {
val myCounter = mutableStateOf(0) // State that can cause recompositions
var lastSeenValue = 0 // Used to track recompositions
composeTestRule.setContent {
Text(myCounter.value.toString())
lastSeenValue = myCounter.value
}
myCounter.value = 1 // The state changes, but there is no recomposition
// Fails because nothing triggered a recomposition
assertTrue(lastSeenValue == 1)
// Passes because the assertion triggers recomposition
composeTestRule.onNodeWithText("1").assertExists()
}
من المهم أيضًا ملاحظة أن هذا الشرط لا ينطبق إلا على التسلسلات الهرمية لإنشاء المحتوى، وليس على باقي التطبيق.
تعطيل المزامنة التلقائية
عند طلب تأكيد أو إجراء من خلال ComposeTestRule
مثل assertExists()
، تتم مزامنة اختبارك مع واجهة مستخدم Compose. في بعض الحالات، قد ترغب في
إيقاف هذه المزامنة والتحكم في الساعة بنفسك. على سبيل المثال، يمكنك التحكم في الوقت لالتقاط لقطات شاشة دقيقة لصورة متحركة في
نقطة تظل فيها واجهة المستخدم مشغولة. لإيقاف المزامنة التلقائية،
اضبط السمة autoAdvance
في mainClock
على false
:
composeTestRule.mainClock.autoAdvance = false
عادةً ما تقوم بعد ذلك بتقديم الوقت بنفسك. ويمكنك التقدّم بإطار واحد فقط باستخدام advanceTimeByFrame()
أو حسب مدة محدّدة باستخدام advanceTimeBy()
:
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
موارد عدم النشاط
يمكن للإنشاء مزامنة الاختبارات وواجهة المستخدم بحيث يتم تنفيذ كل إجراء وتأكيد في حالة عدم النشاط، مع الانتظار أو التقدم على مدار الساعة حسب الحاجة. ومع ذلك، يمكن تشغيل بعض العمليات غير المتزامنة التي تؤثر نتائجها على حالة واجهة المستخدم في الخلفية دون أن يكون الاختبار على دراية بها.
يمكنك إنشاء موارد عدم النشاط وتسجيلها في الاختبار بحيث تؤخذ في الاعتبار عند تحديد ما إذا كان التطبيق الذي يخضع للاختبار مشغولاً أو غير نشِط لفترة قصيرة. ولن تضطر إلى اتخاذ أي إجراء ما لم تكن بحاجة إلى تسجيل موارد إضافية غير نشطة، مثلاً عند تشغيل مهمة في الخلفية غير متزامنة مع Espresso أو Compose.
تشبه واجهة برمجة التطبيقات هذه موارد
وضع الخمول في Espresso للإشارة إلى ما إذا كان الموضوع الذي يخضع للاختبار غير نشط أو مشغول. يمكنك استخدام قاعدة اختبار الإنشاء
لتسجيل تنفيذ
IdlingResource
.
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
المزامنة اليدوية
في بعض الحالات، يجب مزامنة واجهة مستخدم Compose مع الأجزاء الأخرى من الاختبار أو التطبيق الذي تختبره.
ينتظر waitForIdle
إلى أن يكون Compose غير نشِط لفترة قصيرة، ولكن ذلك يعتمد على السمة autoAdvance
:
composeTestRule.mainClock.autoAdvance = true // default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for Idling Resources to become idle
يُرجى العِلم أنّه في كلتا الحالتين، سينتظر waitForIdle
أيضًا إلى حين انتهاء
بطاقات الرسم والتنسيق في انتظار المراجعة.
يمكنك أيضًا ضبط الساعة بشكل مسبق حتى يتم استيفاء شرط معيّن في
advanceTimeUntil()
.
composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
لاحظ أن الشرط المحدد يجب أن يتحقق من الحالة التي يمكن أن تتأثر بهذه الساعة (لا يعمل إلا مع حالة Compose).
في انتظار الشروط
أي شرط يعتمد على العمل الخارجي، مثل تحميل البيانات أو مقياس Android أو الرسم الخاص به (أي القياس أو الرسم خارج إطار الإنشاء)، يجب أن يستخدم مفهومًا أكثر عمومية، مثل waitUntil()
:
composeTestRule.waitUntil(timeoutMs) { condition }
يمكنك أيضًا استخدام أي من
أدوات مساعدة waitUntil
:
composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)
composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)
composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)
composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)
الأنماط الشائعة
يوضّح هذا القسم بعض الأساليب الشائعة التي ستظهر لك في اختبار Compose.
إجراء الاختبار بشكل منفصل
يتيح لك ComposeTestRule
بدء نشاط يعرض أي عنصر مركّب: تطبيقك الكامل أو شاشة واحدة أو عنصر صغير. من الممارسات الجيدة أيضًا التحقق من أن المواد المركبة قد تم تغليفها بشكل صحيح وأنها تعمل بشكل مستقل، مما يسمح باختبار واجهة المستخدم بشكل أسهل وأكثر تركيزًا.
لا يعني هذا أنّه يجب إنشاء اختبارات لواجهة المستخدم الخاصة بالوحدة فقط. اختبارات واجهة المستخدم مهمة جدًا أيضًا في تحديد نطاق أجزاء أكبر من واجهة المستخدم.
يمكنك الوصول إلى النشاط والمراجع بعد ضبط إعدادات المحتوى.
وفي كثير من الأحيان، تحتاج إلى ضبط المحتوى قيد الاختبار باستخدام
composeTestRule.setContent
وتحتاج أيضًا إلى الوصول إلى موارد النشاط،
على سبيل المثال، لتأكيد تطابق النص المعروض مع مورد سلسلة. ومع ذلك،
لا يمكنك استدعاء الدالة setContent
على قاعدة تم إنشاؤها باستخدام
createAndroidComposeRule()
إذا كان النشاط يطلبها مسبقًا.
لتحقيق ذلك، يمكنك إنشاء AndroidComposeTestRule
باستخدام نشاط فارغ
(مثل ComponentActivity
).
class MyComposeTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun myTest() {
// Start the app
composeTestRule.setContent {
MyAppTheme {
MainScreen(uiState = exampleUiState, /*...*/)
}
}
val continueLabel = composeTestRule.activity.getString(R.string.next)
composeTestRule.onNodeWithText(continueLabel).performClick()
}
}
يجب إضافة ComponentActivity
إلى ملف AndroidManifest.xml
الخاص بالتطبيق. يمكنك القيام بذلك عن طريق إضافة هذه
التبعية إلى وحدتك:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
خصائص دلالات الألفاظ المخصّصة
يمكنك إنشاء سمات دلالات مخصّصة لعرض المعلومات للاختبارات. لتنفيذ ذلك، حدِّد سمة SemanticsPropertyKey
جديدة واجعلها متاحة باستخدام السمة SemanticsPropertyReceiver
.
// Creates a Semantics property of type Long
val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
var SemanticsPropertyReceiver.pickedDate by PickedDateKey
يمكنك الآن استخدام هذه السمة باستخدام مفتاح التعديل semantics
:
val datePickerValue by remember { mutableStateOf(0L) }
MyCustomDatePicker(
modifier = Modifier.semantics { pickedDate = datePickerValue }
)
من الاختبارات، يمكنك استخدام SemanticsMatcher.expectValue
لتأكيد قيمة السمة:
composeTestRule
.onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21
.assertExists()
التأكّد من استعادة الحالة
عليك التحقّق من استعادة حالة عناصر الإنشاء بشكل صحيح
عند إعادة إنشاء النشاط أو العملية. ويمكن إجراء عملية الفحص هذه بدون الاعتماد على
الأنشطة الترفيهية في الفئة
StateRestorationTester
.
تتيح لك هذه الفئة محاكاة إنشاء منتج. ومن المفيد بشكل خاص التحقّق من تنفيذ rememberSaveable
.
class MyStateRestorationTests {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun onRecreation_stateIsRestored() {
val restorationTester = StateRestorationTester(composeTestRule)
restorationTester.setContent { MainScreen() }
// TODO: Run actions that modify the state
// Trigger a recreation
restorationTester.emulateSavedInstanceStateRestore()
// TODO: Verify that state has been correctly restored.
}
}
تصحيح الأخطاء
الطريقة الرئيسية لحل المشكلات في اختباراتك هي إلقاء نظرة على شجرة المعاني.
يمكنك طباعة الشجرة من خلال استدعاء composeTestRule.onRoot().printToLog()
في أي مرحلة من مراحل الاختبار. تطبع هذه الدالة سجلاً مثل هذا:
Node #1 at (...)px
|-Node #2 at (...)px
OnClick = '...'
MergeDescendants = 'true'
|-Node #3 at (...)px
| Text = 'Hi'
|-Node #5 at (83.0, 86.0, 191.0, 135.0)px
Text = 'There'
وتحتوي هذه السجلات على معلومات قيّمة لتتبُّع الأخطاء.
إمكانية التشغيل التفاعلي مع قهوة إسبرسو
في التطبيق المختلط، يمكنك العثور على مكونات Compose داخل العروض الهرمية للعرض
وطرق العرض داخل العناصر Compose (عبر إنشاء AndroidView
).
ليست هناك خطوات خاصة مطلوبة لمطابقة أي من النوعين. يمكنك مطابقة طرق العرض من خلال onView
في Espresso، وعناصر Compose عبر ComposeTestRule
.
@Test
fun androidViewInteropTest() {
// Check the initial state of a TextView that depends on a Compose state:
Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
// Click on the Compose button that changes the state
composeTestRule.onNodeWithText("Click here").performClick()
// Check the new value
Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
}
إمكانية التشغيل التفاعلي باستخدام UiAutomator
وفقًا للإعدادات التلقائية، لا يمكن الوصول إلى المواد الإبداعية من
UiAutomator إلا باستخدام الأوصاف الملائمة لها (النص المعروض ووصف المحتوى وما إلى ذلك). إذا أردت الوصول إلى أي مادة مركّبة تستخدم السمة Modifier.testTag
، عليك تفعيل السمة الدلالية testTagAsResourceId
للشجرة الفرعية المحدّدة للمحتوى المركّب.
يكون تفعيل هذا السلوك مفيدًا في الإنشاءات التي ليس لها أي اسم معرِّف فريد آخر، مثل العناصر القابلة للتمرير (على سبيل المثال، LazyColumn
).
يمكنك تفعيلها مرة واحدة فقط في التسلسل الهرمي للمحتوى القابل للإنشاء لضمان إمكانية الوصول إليها من UiAutomator،
وذلك من خلال استخدام Modifier.testTag
.
Scaffold(
// Enables for all composables in the hierarchy.
modifier = Modifier.semantics {
testTagsAsResourceId = true
}
){
// Modifier.testTag is accessible from UiAutomator for composables nested here.
LazyColumn(
modifier = Modifier.testTag("myLazyColumn")
){
// content
}
}
يمكن الوصول إلى أي عنصر يتم إنشاؤه من خلال Modifier.testTag(tag)
باستخدام By.res(resourceName)
باستخدام tag
نفسه المُستخدَم في resourceName
.
val device = UiDevice.getInstance(getInstrumentation())
val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
// some interaction with the lazyColumn
مزيد من المعلومات
للاطّلاع على مزيد من المعلومات، يمكنك تجربة الدرس التطبيقي حول الترميز الخاص باختبار Jetpack Compose.
عيّنات
أفلام مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عند إيقاف JavaScript.
- الدلالات الدلالية في Compose
- داخل النوافذ في Compose
- اعتبارات أخرى