نوشتن آزمایش‌های خودکار با UI Automator

چارچوب آزمایشی UI Automator مجموعه‌ای از میاناهای برنامه‌سازی کاربردی را برای ساختن آزمایش‌های میانای کاربر ارائه می‌دهد که با برنامه‌های کاربر و برنامه‌های سیستم تعامل برقرار می‌کنند.

مقدمه‌ای بر آزمایش مدرن UI Automator

UI Automator 2.4 یک «زبان خاص دامنه» (DSL) ساده‌شده و سازگار با Kotlin را معرفی می‌کند که نوشتن آزمایش‌های میانای کاربر برای Android را ساده می‌کند. این سطح جدید میانای برنامه‌سازی کاربردی بر یافتن عنصر مبتنی بر گزاره و کنترل صریح بر وضعیت‌های برنامه تمرکز دارد. از آن برای ایجاد آزمایش‌های خودکار قابل‌نگهداری‌تر و قابل‌اعتمادتر استفاده کنید.

UI Automator به شما امکان می‌دهد برنامه را از خارج از فرایند برنامه آزمایش کنید. این به شما امکان می‌دهد نسخه‌های پخش را با اعمال کوچک‌سازی آزمایش کنید. UI Automator همچنین به نوشتن آزمایش‌های محک کلان کمک می‌کند.

ویژگی‌های کلیدی رویکرد مدرن عبارت‌اند از:

  • محدوده آزمایشی اختصاصی uiAutomator برای کد آزمایشی تمیزتر و گویاتر.
  • روش‌هایی مثل onElement، onElements، و onElementOrNull برای پیدا کردن عناصر میانای کاربر با گزاره‌های واضح.
  • مکانیزم انتظار داخلی برای عناصر شرطی onElement*(timeoutMs: Long = 10000)
  • مدیریت صریح وضعیت برنامه مانند waitForStable و waitForAppToBeVisible.
  • تعامل مستقیم با گره‌های پنجره دسترس‌پذیری برای سناریوهای آزمایش چندپنجره‌ای.
  • قابلیت‌های نماگرفت داخلی و ResultsReporter برای آزمایش بصری و اشکال‌زدایی.

راه‌اندازی پروژه

برای شروع استفاده از میاناهای برنامه‌سازی کاربردی مدرن UI Automator، فایل build.gradle.kts پروژه‌تان را به‌روز کنید تا جدیدترین وابستگی را دربرگیرد:

کاتلین

dependencies {
  ...
  androidTestImplementation("androidx.test.uiautomator:uiautomator:2.4.0-alpha05")
}

شیک

dependencies {
  ...
  androidTestImplementation "androidx.test.uiautomator:uiautomator:2.4.0-alpha05"
}

مفاهیم اصلی میانای برنامه‌سازی کاربردی

بخش‌های زیر مفاهیم اصلی API مدرن UI Automator را شرح می‌دهند.

محدوده آزمایشی uiAutomator

به همه میاناهای برنامه‌سازی کاربردی UI Automator جدید در بلوک uiAutomator { ... } دسترسی پیدا کنید. این تابع یک UiAutomatorTestScope ایجاد می‌کند که محیطی مختصر و ایمن ازنظر نوع برای عملیات آزمایشی شما فراهم می‌کند.

uiAutomator {
  // All your UI Automator actions go here
  startApp("com.example.targetapp")
  onElement { textAsString() == "Hello, World!" }.click()
}

یافتن عناصر میانای کاربر

از میاناهای برنامه‌سازی کاربردی UI Automator با گزاره‌ها برای یافتن عناصر میانای کاربر استفاده کنید. این گزاره‌ها به شما امکان می‌دهند شرایطی را برای ویژگی‌هایی مانند نوشتار، حالت انتخاب‌شده یا متمرکز، و شرح محتوا تعریف کنید.

  • onElement { predicate }: اولین عنصر میانای کاربر را که با گزاره در مهلت پیش‌فرض مطابقت دارد برمی‌گرداند. اگر تابع عنصر منطبقی را پیدا نکند، استثنایی را ایجاد می‌کند.

    // Find a button with the text "Submit" and click it
    onElement { textAsString() == "Submit" }.click()
    
    // Find a UI element by its resource ID
    onElement { viewIdResourceName == "my_button_id" }.click()
    
    // Allow a permission request
    watchFor(PermissionDialog) {
      clickAllow()
    }
    
  • ‫onElementOrNull { predicate }: مشابه onElement، اما اگر تابع هیچ عنصر منطبقی را در مهلت زمانی پیدا نکند، null را برمی‌گرداند. استثنایی قائل نمی‌شود. از این روش برای عناصر اختیاری استفاده کنید.

    val optionalButton = onElementOrNull { textAsString() == "Skip" }
    optionalButton?.click() // Click only if the button exists
    
  • onElements { predicate }: تا زمانی که حداقل یک عنصر میانای کاربر با گزاره داده‌شده مطابقت داشته باشد منتظر می‌ماند، سپس فهرستی از همه عناصر میانای کاربر منطبق را برمی‌گرداند.

    // Get all items in a list Ui element
    val listItems = onElements { className == "android.widget.TextView" && isClickable }
    listItems.forEach { it.click() }
    

در اینجا چند نکته برای استفاده از تماس‌های onElement آورده شده است:

  • زنجیر کردن فراخوانی‌های onElement برای عناصر تودرتو: می‌توانید فراخوانی‌های onElement را زنجیر کنید تا عناصر را در عناصر دیگر پیدا کنید، با دنبال کردن سلسله‌مراتب والد-فرزند.

    // Find a parent Ui element with ID "first", then its child with ID "second",
    // then its grandchild with ID "third", and click it.
    onElement { viewIdResourceName == "first" }
      .onElement { viewIdResourceName == "second" }
      .onElement { viewIdResourceName == "third" }
      .click()
    
  • با ارسال مقداری که نشان‌دهنده میلی‌ثانیه است، زمان وقفه برای توابع onElement* مشخص کنید.

    // Find a Ui element with a zero timeout (instant check)
    onElement(0) { viewIdResourceName == "something" }.click()
    
    // Find a Ui element with a custom timeout of 10 seconds
    onElement(10_000) { textAsString() == "Long loading text" }.click()
    

تعامل با عناصر میانای کاربر

با شبیه‌سازی کلیک‌ها یا تنظیم نوشتار در فیلدهای قابل‌ویرایش، با عناصر میانای کاربر تعامل برقرار کنید.

// Click a Ui element
onElement { textAsString() == "Tap Me" }.click()

// Set text in an editable field
onElement { className == "android.widget.EditText" }.setText("My input text")

// Perform a long click
onElement { contentDescription == "Context Menu" }.longClick()

مدیریت وضعیت‌های برنامه و ناظران

چرخه حیات برنامه‌تان را مدیریت کنید و عناصر میانای کاربر غیرمنتظره‌ای را که ممکن است درطول آزمایش‌هایتان ظاهر شوند کنترل کنید.

مدیریت چرخه حیات برنامه

میاناهای برنامه‌سازی کاربردی روش‌هایی برای کنترل وضعیت برنامه تحت آزمایش ارائه می‌دهند:

// Start a specific app by package name. Used for benchmarking and other
// self-instrumenting tests.
startApp("com.example.targetapp")

// Start a specific activity within the target app
startActivity(SomeActivity::class.java)

// Start an intent
startIntent(myIntent)

// Clear the app's data (resets it to a fresh state)
clearAppData("com.example.targetapp")

رسیدگی به واسط کاربر غیرمنتظره

watchFor API به شما امکان می‌دهد کنترل‌کننده‌هایی برای عناصر میانای کاربر غیرمنتظره، مثل کادرهای گفتگوی اجازه، تعریف کنید که ممکن است درطول جریان آزمایشی شما ظاهر شوند. این از سازوکار ناظر داخلی استفاده می‌کند اما انعطاف‌پذیری بیشتری ارائه می‌دهد.

import androidx.test.uiautomator.PermissionDialog

@Test
fun myTestWithPermissionHandling() = uiAutomator {
  startActivity(MainActivity::class.java)

  // Register a watcher to click "Allow" if a permission dialog appears
  watchFor(PermissionDialog) { clickAllow() }

  // Your test steps that might trigger a permission dialog
  onElement { textAsString() == "Request Permissions" }.click()

  // Example: You can register a different watcher later if needed
  clearAppData("com.example.targetapp")

  // Now deny permissions
  startApp("com.example.targetapp")
  watchFor(PermissionDialog) { clickDeny() }
  onElement { textAsString() == "Request Permissions" }.click()
}

PermissionDialog نمونه‌ای از ScopedWatcher<T> است، که در آن T شیء ارسال‌شده به‌عنوان دامنه به بلوک در watchFor است. می‌توانید براساس این الگو، ناظران سفارشی ایجاد کنید.

منتظر رؤیت‌پذیری و پایداری برنامه بمانید

گاهی‌اوقات آزمایش‌ها باید منتظر بمانند تا عناصر نمایان یا پایدار شوند. UI Automator چندین API برای کمک به این کار ارائه می‌دهد.

waitForAppToBeVisible("com.example.targetapp") منتظر می‌ماند تا عنصر میانای کاربر با نام بسته ارائه‌شده در مدت زمان قابل‌سفارشی‌سازی در صفحه ظاهر شود.

// Wait for the app to be visible after launching it
startApp("com.example.targetapp")
waitForAppToBeVisible("com.example.targetapp")

از waitForStable() API استفاده کنید تا مطمئن شوید که واسط کاربر برنامه قبل‌از تعامل با آن پایدار درنظر گرفته می‌شود.

// Wait for the entire active window to become stable
activeWindow().waitForStable()

// Wait for a specific Ui element to become stable (e.g., after a loading animation)
onElement { viewIdResourceName == "my_loading_indicator" }.waitForStable()

استفاده از UI Automator برای Macrobenchmarks و «نمایه‌های پایه»

از UI Automator برای آزمایش عملکرد با Jetpack Macrobenchmark و برای تولید Baseline Profiles استفاده کنید، زیرا روشی قابل‌اعتماد برای تعامل با برنامه و اندازه‌گیری عملکرد از دیدگاه کاربر نهایی ارائه می‌دهد.

Macrobenchmark از میاناهای برنامه‌سازی کاربردی UI Automator برای هدایت واسط کاربر و اندازه‌گیری تعامل‌ها استفاده می‌کند. برای مثال، در محک‌های راه‌اندازی، می‌توانید از onElement برای تشخیص زمان بار شدن کامل محتوای واسط کاربر استفاده کنید و به شما امکان می‌دهد زمان نمایش کامل (TTFD) را اندازه‌گیری کنید. در محک‌های تأخیر، از میاناهای برنامه‌سازی کاربردی UI Automator برای پیمایش فهرست‌ها یا اجرای پویانمایی‌ها برای اندازه‌گیری زمان‌بندی‌های فریم استفاده می‌شود. توابعی مانند startActivity() یا startIntent() برای قرار دادن برنامه در وضعیت صحیح قبل‌از شروع اندازه‌گیری مفید هستند.

هنگام تولید «نمایه‌های پایه»، مسیرهای قطعی کاربر (CUJ) برنامه‌تان را خودکارسازی می‌کنید تا ثبت کنید کدام کلاس‌ها و روش‌ها به پیش‌کامپایل نیاز دارند. UI Automator ابزاری ایده‌آل برای نوشتن این اسکریپت‌های خودکارسازی است. یافتن عنصر مبتنی بر گزاره DSL مدرن و سازوکارهای انتظار داخلی (onElement) درمقایسه با روش‌های دیگر، به اجرای آزمایشی قوی‌تر و قطعی‌تر منجر می‌شود. این پایداری، ناپایداری را کاهش می‌دهد و تضمین می‌کند که «نمایه پایه» تولیدشده به‌طور دقیق مسیرهای کد اجراشده درطول مهم‌ترین جریان‌های کاربر شما را منعکس می‌کند.

ویژگی‌های پیشرفته

ویژگی‌های زیر برای سناریوهای آزمایشی پیچیده‌تر مفید هستند.

تعامل با چند پنجره

UI Automator API به شما امکان می‌دهد مستقیماً با عناصر میانای کاربر تعامل داشته باشید و آن‌ها را بازرسی کنید. این امر به‌ویژه برای سناریوهایی که شامل چندین پنجره هستند، مانند حالت «تصویردرتصویر» (PiP) یا چیدمان‌های صفحه دونیمه، مفید است.

// Find the first window that is in Picture-in-Picture mode
val pipWindow = windows()
  .first { it.isInPictureInPictureMode == true }

// Now you can interact with elements within that specific window
pipWindow.onElement { textAsString() == "Play" }.click()

نماگرفت‌ها و ادعاهای تصویری

نماگرفت‌هایی از کل صفحه‌نمایش، پنجره‌های خاص، یا عناصر میانای کاربر به‌صورت جداگانه مستقیماً در آزمایش‌هایتان بگیرید. این کار برای آزمایش رگرسیون بصری و اشکال‌زدایی مفید است.

uiautomator {
  // Take a screenshot of the entire active window
  val fullScreenBitmap: Bitmap = activeWindow().takeScreenshot()
  fullScreenBitmap.saveToFile(File("/sdcard/Download/full_screen.png"))

  // Take a screenshot of a specific UI element (e.g., a button)
  val buttonBitmap: Bitmap = onElement { viewIdResourceName == "my_button" }.takeScreenshot()
  buttonBitmap.saveToFile(File("/sdcard/Download/my_button_screenshot.png"))

  // Example: Take a screenshot of a PiP window
  val pipWindowScreenshot = windows()
    .first { it.isInPictureInPictureMode == true }
    .takeScreenshot()
  pipWindowScreenshot.saveToFile(File("/sdcard/Download/pip_screenshot.png"))
}

تابع افزونه saveToFile برای Bitmap ذخیره کردن تصویر ثبت‌شده در مسیر مشخص‌شده را ساده می‌کند.

استفاده از ResultsReporter برای اشکال‌زدایی

ResultsReporter به شما کمک می‌کند تا مصنوعات آزمایشی، مانند نماگرفت‌ها، را مستقیماً با نتایج آزمایشی خود در «استودیو Android» مرتبط کنید تا بازرسی و اشکال‌زدایی آسان‌تر شود.

uiAutomator {
  startApp("com.example.targetapp")

  val reporter = ResultsReporter("MyTestArtifacts") // Name for this set of results
  val file = reporter.addNewFile(
    filename = "my_screenshot",
    title = "Accessible button image" // Title that appears in Android Studio test results
  )

  // Take a screenshot of an element and save it using the reporter
  onElement { textAsString() == "Accessible button" }
    .takeScreenshot()
    .saveToFile(file)

  // Report the artifacts to instrumentation, making them visible in Android Studio
  reporter.reportToInstrumentation()
}

انتقال از نسخه‌های قدیمی‌تر UI Automator

اگر آزمایش‌های UI Automator موجودی دارید که با سطوح API قدیمی‌تر نوشته شده‌اند، از جدول زیر به‌عنوان مرجعی برای انتقال به رویکرد مدرن استفاده کنید:

نوع اقدام روش قدیمی UI Automator روش جدید UI Automator
نقطه ورودی UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) منطق آزمایش را در محدوده uiAutomator { ... } قرار دهید.
یافتن عناصر میانای کاربر device.findObject(By.res("com.example.app:id/my_button")) onElement { viewIdResourceName == "my\_button" }
یافتن عناصر میانای کاربر device.findObject(By.text("Click Me")) onElement { textAsString() == "Click Me" }
Wait for idle UI device.waitForIdle() سازوکار مهلت توکار onElement را ترجیح دهید؛ درغیراین‌صورت، activeWindow().waitForStable()
یافتن عناصر فرزند findObject تماس تودرتو به‌صورت دستی ‫onElement().onElement() زنجیره‌ای
رسیدگی به کادرهای گفتگوی اجازه UiAutomator.registerWatcher() watchFor(PermissionDialog)