เขียนการทดสอบอัตโนมัติด้วย UI Automator

เฟรมเวิร์กการทดสอบ UI Automator มีชุด API สำหรับสร้างการทดสอบ UI ที่ โต้ตอบกับแอปของผู้ใช้และแอปของระบบ

ข้อมูลเบื้องต้นเกี่ยวกับการทดสอบ UI Automator สมัยใหม่

UI Automator 2.4 ขอแนะนำ Domain Specific Language (DSL) ที่ปรับปรุงแล้วและเป็นมิตรกับ Kotlin ซึ่งช่วยลดความซับซ้อนในการเขียนการทดสอบ UI สำหรับ Android API Surface ใหม่นี้ มุ่งเน้นที่การค้นหาองค์ประกอบตามตัวบ่งชี้และการควบคุมสถานะแอปอย่างชัดเจน ใช้เพื่อสร้างการทดสอบอัตโนมัติที่ดูแลรักษาได้ง่ายและเชื่อถือได้มากขึ้น

UI Automator ช่วยให้คุณทดสอบแอปจากภายนอกกระบวนการของแอปได้ ซึ่งจะช่วยให้คุณทดสอบเวอร์ชันที่เผยแพร่โดยใช้การลดขนาดได้ นอกจากนี้ UI Automator ยังช่วยในการเขียนการทดสอบ Macrobenchmark ด้วย

ฟีเจอร์สำคัญของแนวทางที่ทันสมัยมีดังนี้

  • uiAutomatorขอบเขตการทดสอบเฉพาะเพื่อโค้ดทดสอบที่สะอาดและสื่อความหมายมากขึ้น
  • เมธอดต่างๆ เช่น onElement, onElements และ onElementOrNull สำหรับการค้นหาองค์ประกอบ UI ที่มีเพรดิเคตที่ชัดเจน
  • กลไกการรอในตัวสำหรับองค์ประกอบแบบมีเงื่อนไข onElement*(timeoutMs: Long = 10000)
  • การจัดการสถานะของแอปอย่างชัดเจน เช่น waitForStable และ waitForAppToBeVisible
  • การโต้ตอบโดยตรงกับโหนดหน้าต่างการช่วยเหลือพิเศษสำหรับสถานการณ์การทดสอบแบบหลายหน้าต่าง
  • ความสามารถในการถ่ายภาพหน้าจอในตัวและ ResultsReporter สำหรับการทดสอบภาพ และการแก้ไขข้อบกพร่อง

สร้างโปรเจ็กต์

หากต้องการเริ่มใช้ UI Automator API ที่ทันสมัย ให้อัปเดตไฟล์ build.gradle.kts ของโปรเจ็กต์ให้มีการอ้างอิงล่าสุด

Kotlin

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

Groovy

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

แนวคิดหลักของ API

ส่วนต่อไปนี้จะอธิบายแนวคิดหลักของ Modern UI Automator API

ขอบเขตการทดสอบ uiAutomator

เข้าถึง API ของ UI Automator ใหม่ทั้งหมดภายในบล็อก uiAutomator { ... } ฟังก์ชันนี้จะสร้าง UiAutomatorTestScope ที่มีสภาพแวดล้อมที่กระชับ และปลอดภัยสำหรับประเภทการดำเนินการทดสอบ

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

ค้นหาองค์ประกอบ UI

ใช้ UI Automator API กับ Predicate เพื่อค้นหาองค์ประกอบ UI โดยตัวบ่งชี้เหล่านี้ ช่วยให้คุณกำหนดเงื่อนไขสำหรับพร็อพเพอร์ตี้ เช่น ข้อความ สถานะที่เลือกหรือโฟกัส และคำอธิบายเนื้อหา

  • onElement { predicate }: แสดงผลองค์ประกอบ UI แรกที่ตรงกับ เพรดิเคตภายในระยะหมดเวลาเริ่มต้น ฟังก์ชันจะส่งข้อยกเว้นหากไม่พบองค์ประกอบที่ตรงกัน

    // 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 }: รอจนกว่าองค์ประกอบ UI อย่างน้อย 1 รายการจะตรงกับ เพรดิเคตที่ระบุ จากนั้นจะแสดงผลรายการองค์ประกอบ UI ที่ตรงกันทั้งหมด

    // 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()
    

โต้ตอบกับองค์ประกอบ UI

โต้ตอบกับองค์ประกอบ UI โดยจำลองการคลิกหรือตั้งค่าข้อความใน ช่องที่แก้ไขได้

// 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()

จัดการสถานะแอปและ Watcher

จัดการวงจรของแอปและจัดการองค์ประกอบ UI ที่ไม่คาดคิดซึ่งอาจปรากฏขึ้นระหว่างการทดสอบ

การจัดการวงจรของแอป

API มีวิธีควบคุมสถานะของแอปที่อยู่ระหว่างการทดสอบดังนี้

// 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")

จัดการ UI ที่ไม่คาดคิด

watchFor API ช่วยให้คุณกำหนดตัวแฮนเดิลสำหรับองค์ประกอบ UI ที่ไม่คาดคิดได้ เช่น กล่องโต้ตอบสิทธิ์ ซึ่งอาจปรากฏขึ้นระหว่างขั้นตอนการทดสอบ ซึ่ง ใช้กลไกการตรวจสอบภายในแต่มีความยืดหยุ่นมากกว่า

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") รอให้องค์ประกอบ UI ที่มี ชื่อแพ็กเกจที่ระบุปรากฏบนหน้าจอภายในระยะหมดเวลาที่ปรับแต่งได้

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

ใช้ waitForStable() API เพื่อยืนยันว่า UI ของแอปถือว่าเสถียรก่อนที่จะโต้ตอบกับ UI

// 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()

ฟีเจอร์ขั้นสูง

ฟีเจอร์ต่อไปนี้มีประโยชน์สำหรับสถานการณ์การทดสอบที่ซับซ้อนมากขึ้น

โต้ตอบกับหลายหน้าต่าง

API ของ UI Automator ช่วยให้คุณโต้ตอบและตรวจสอบองค์ประกอบ UI ได้โดยตรง ซึ่งจะมีประโยชน์อย่างยิ่งสำหรับสถานการณ์ที่เกี่ยวข้องกับหลายหน้าต่าง เช่น โหมดการแสดงภาพซ้อนภาพ (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()

ภาพหน้าจอและการยืนยันด้วยภาพ

ถ่ายภาพหน้าจอทั้งหน้าจอ หน้าต่างที่เฉพาะเจาะจง หรือ องค์ประกอบ UI แต่ละรายการได้โดยตรงภายในเทสต์ ซึ่งจะเป็นประโยชน์ต่อการทดสอบและการแก้ไขข้อบกพร่องของ การถดถอยของภาพ

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 สำหรับบิตแมปช่วยให้บันทึกรูปภาพที่แคปเจอร์ไปยังเส้นทางที่ระบุได้ง่ายขึ้น

ใช้ ResultsReporter เพื่อการแก้ไขข้อบกพร่อง

ResultsReporter ช่วยให้คุณเชื่อมโยงอาร์ติแฟกต์การทดสอบ เช่น ภาพหน้าจอ กับผลการทดสอบใน Android Studio ได้โดยตรง เพื่อให้ตรวจสอบและ แก้ไขข้อบกพร่องได้ง่ายขึ้น

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 { ... }
ค้นหาองค์ประกอบ UI device.findObject(By.res("com.example.app:id/my_button")) onElement { viewIdResourceName == "my\_button" }
ค้นหาองค์ประกอบ UI device.findObject(By.text("Click Me")) onElement { textAsString() == "Click Me" }
รอ UI ที่ไม่ได้ใช้งาน device.waitForIdle() ต้องการใช้กลไกการหมดเวลาในตัวของ onElement หรือไม่ก็ activeWindow().waitForStable()
ค้นหาองค์ประกอบย่อย การซ้อนfindObjectการโทรด้วยตนเอง onElement().onElement() การเชื่อมโยง
จัดการกล่องโต้ตอบสิทธิ์ UiAutomator.registerWatcher() watchFor(PermissionDialog)