Kiểm thử trong Jetpack Compose

1. Giới thiệu và thiết lập

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách kiểm thử giao diện người dùng tạo bằng Jetpack Compose. Bạn sẽ viết các chương trình kiểm thử đầu tiên trong quá trình tìm hiểu về kiểm thử tách biệt, kiểm thử gỡ lỗi, cây ngữ nghĩa và đồng bộ hoá.

Bạn cần có

Xem xét mã nguồn cho lớp học lập trình này (Rally)

Bạn sẽ dùng nghiên cứu của Material về Rally làm cơ sở cho lớp học lập trình này. Bạn sẽ tìm thấy nghiên cứu này trong kho lưu trữ android-compose-codelabs của GitHub. Để tạo bản sao, hãy chạy:

git clone https://github.com/android/codelab-android-compose.git

Sau khi tải xuống, hãy mở dự án TestingCodelab.

Ngoài ra, bạn có thể tải 2 tệp zip xuống:

Mở thư mục TestingCodelab, trong đó có một ứng dụng tên là Rally.

Tìm hiểu cấu trúc dự án

Kiểm thử Compose là một hình thức kiểm thử đo lường. Tức là bạn phải có một thiết bị (thiết bị thực tế hoặc trình mô phỏng) để tiến hành kiểm thử trên đó.

Rally đã chứa sẵn một số kiểm thử đo lường cho giao diện người dùng. Bạn có thể tìm thấy các hoạt động kiểm thử này trong nhóm tài nguyên androidTest:

b14721ae60ee9022.png

Đây là thư mục nơi bạn sẽ tiến hành các hoạt động kiểm thử mới. Vui lòng xem tệp AnimatingCircleTests.kt để tìm hiểu về kiểm thử Compose.

Rally đã được định sẵn cấu hình, tất cả những gì bạn cần để bật tính năng kiểm thử Compose trong một dự án mới chính là các thành phần phụ thuộc kiểm thử trong tệp build.gradle của mô-đun liên quan, bao gồm:

androidTestImplementation "androidx.compose.ui:ui-test-junit4:$version"

debugImplementation "androidx.compose.ui:ui-test-manifest:$rootProject.composeVersion"

Bạn có thể thử chạy và làm quen với ứng dụng.

2. Kiểm thử những gì?

Chúng ta sẽ tập trung vào thanh thẻ của Rally, bao gồm một dãy thẻ (Overview, Accounts và Bills (Tổng quan, Tài khoản và Hoá đơn)). Trong trường hợp này, thanh này trông như sau:

19c6a7eb9d732d37.gif

Trong lớp học lập trình này, bạn sẽ kiểm thử giao diện người dùng của thanh này.

Quá trình này có thể bao gồm nhiều công đoạn:

  • Kiểm thử xem các thẻ có thể hiện biểu tượng và văn bản như mong muốn không
  • Kiểm thử xem ảnh động có khớp với quy cách không
  • Kiểm thử xem các sự kiện điều hướng có được kích hoạt chính xác hay không
  • Kiểm thử vị trí và khoảng cách của các thành phần trên giao diện người dùng theo nhiều trạng thái
  • Chụp ảnh màn hình thanh thẻ rồi so sánh với ảnh chụp màn hình trước đó

Không có quy tắc chuẩn mực nào về việc phải kiểm thử một thành phần như thế nào và bao nhiêu lần Bạn có thể làm tất cả những việc trên! Trong lớp học lập trình này, bạn sẽ kiểm thử xem logic của một trạng thái có chính xác hay không bằng cách xác minh rằng:

  • Thẻ chỉ hiện nhãn khi được chọn.
  • Màn hình đang hoạt động xác định thẻ được chọn

3. Tạo một kiểm thử giao diện người dùng đơn giản

Tạo tệp TopAppBarTest

Tạo một tệp mới trong cùng thư mục với AnimatingCircleTests.kt (app/src/androidTest/com/example/compose/rally) và gọi tệp đó là TopAppBarTest.kt.

Bạn có thể tạo một Compose tương ứng với một ComposeTestRule bằng cách gọi phương thức createComposeRule(). Quy tắc này cho phép bạn thiết lập nội dung Compose đang kiểm thử và tương tác với nội dung đó.

Thêm ComposeTestRule

package com.example.compose.rally

import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class TopAppBarTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    // TODO: Add tests
}

Kiểm thử tách biệt

Ví dụ như trong chương trình kiểm thử Compose, chúng ta có thể bắt đầu hoạt động chính của ứng dụng tương tự như cách bạn thực hiện trong Khung hiển thị trên Android bằng Espresso. Bạn có thể thực hiện việc này thông qua createAndroidComposeRule.

// Don't copy this over

@get:Rule
val composeTestRule = createAndroidComposeRule(RallyActivity::class.java)

Tuy nhiên, với Compose, chúng ta có thể đơn giản hoá mọi thứ một cách đáng kể thông qua việc kiểm thử thành phần tách biệt. Bạn có thể chọn loại nội dung giao diện người dùng Compose để sử dụng trong quá trình kiểm thử. Phương thức setContent của ComposeTestRule sẽ giúp bạn thực hiện việc này và bạn có thể gọi phương thức này ở bất cứ đâu (nhưng chỉ một lần).

// Don't copy this over

class TopAppBarTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    
    @Test
    fun myTest() {
        composeTestRule.setContent { 
            Text("You can set any Compose content!")
        }
    }
}

Chúng ta muốn kiểm thử TopAppBar nên sẽ tập trung vào phần này. Hãy gọi RallyTopAppBar bên trong setContent và để Android Studio điền tên tham số.

import androidx.compose.ui.test.junit4.createComposeRule
import com.example.compose.rally.ui.components.RallyTopAppBar
import org.junit.Rule
import org.junit.Test

class TopAppBarTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun rallyTopAppBarTest() {
        composeTestRule.setContent {
            RallyTopAppBar(
                allScreens = ,
                onTabSelected = { /*TODO*/ },
                currentScreen =
            )
        }
    }
}

Tầm quan trọng của thành phần kết hợp kiểm thử được (testable Composable)

RallyTopAppBar lấy 3 tham số dễ cung cấp để chúng ta có thể truyền dữ liệu giả mà mình kiểm soát. Ví dụ:

    @Test
    fun rallyTopAppBarTest() {
        val allScreens = RallyScreen.values().toList()
        composeTestRule.setContent { 
            RallyTopAppBar(
                allScreens = allScreens,
                onTabSelected = { },
                currentScreen = RallyScreen.Accounts
            )
        }
        Thread.sleep(5000)
    }

Chúng ta cũng thêm phương thức sleep() để có thể thấy được những gì đang diễn ra. Nhấp chuột phải vào rallyTopAppBarTest rồi nhấp vào "Run rallyTopAppBarTest()..." ("Chạy rallyTopAppBarTest()...").

baca545ddc8c3fa9.png

Kết quả kiểm thử cho thấy thanh ứng dụng xuất hiện ở trên cùng (trong 5 giây), nhưng không như chúng ta mong đợi: thanh ứng dụng đang có giao diện sáng!

Lý do là thanh ứng dụng này được xây dựng bằng các Thành phần Material và dự kiến sẽ nằm trong MaterialTheme. Nếu không, thanh này sẽ chuyển về màu theo kiểu "cơ sở" (baseline).

MaterialTheme có các lựa chọn mặc định phù hợp nên sẽ không gặp sự cố. Chúng ta sẽ không kiểm thử giao diện hoặc chụp ảnh màn hình nên có thể bỏ qua phần này và sử dụng giao diện sáng theo mặc định. Bạn có thể kết hợp RallyTopAppBar với RallyTheme để khắc phục vấn đề này.

Xác minh thẻ đã được chọn

Việc tìm kiếm các thành phần giao diện người dùng, kiểm tra các thuộc tính và thực hiện thao tác sẽ được thực hiện thông qua quy tắc kiểm thử, tuân theo mẫu dưới đây:

composeTestRule{.finder}{.assertion}{.action}

Trong phần kiểm thử này, bạn sẽ tìm từ "Accounts" ("Tài khoản") để kiểm tra nhãn của thẻ này được hiển thị hay không.

baca545ddc8c3fa9.png

Một cách hiệu quả khác để biết công cụ nào đang có sẵn là sử dụng Bản tóm tắt về chương trình kiểm thử Compose hoặc tài liệu tham khảo về gói kiểm thử. Trình tìm kiếm (finder) và câu nhận định (assertion) cũng có thể giúp ích cho trong tình huống này. Ví dụ: onNodeWithText, onNodeWithContentDescription, isSelected, hasContentDescription, assertIsSelected...

Mỗi thẻ có một nội dung mô tả riêng:

  • Overview (Tổng quan)
  • Accounts (Tài khoản)
  • Bills (Hoá đơn)

Với thông tin này, hãy thay thế Thread.sleep(5000) bằng câu lệnh tìm kiếm nội dung mô tả nội dung và xác nhận rằng nội dung đó tồn tại:

import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.onNodeWithContentDescription
...

@Test
fun rallyTopAppBarTest_currentTabSelected() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNodeWithContentDescription(RallyScreen.Accounts.name)
        .assertIsSelected()
}

Bây giờ, hãy chạy lại chương trình kiểm thử và bạn sẽ thấy thanh trạng thái kiểm thử màu xanh lục:

75bab3b37e795b65.png

Xin chúc mừng! Bạn đã viết chương trình kiểm thử Compose đầu tiên. Bạn đã tìm hiểu cách kiểm thử tách biệt cũng như cách sử dụng trình tìm kiếm và câu nhận định.

Công việc này tuy đơn giản nhưng bạn cần trang bị trước một số kiến thức về thành phần (phần mô tả nội dung và thuộc tính đã chọn). Ở bước tiếp theo, bạn sẽ tìm hiểu cách kiểm tra những thuộc tính nào đang có sẵn.

4. Kiểm thử gỡ lỗi

Trong bước này, bạn sẽ xác minh xem nhãn của thẻ hiện tại có đang thể hiện dưới dạng viết hoa hay không.

baca545ddc8c3fa9.png

Một giải pháp khả thi là cố gắng tìm và xác nhận rằng tồn tại văn bản đó:

import androidx.compose.ui.test.onNodeWithText
...

@Test
fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNodeWithText(RallyScreen.Accounts.name.uppercase())
        .assertExists()
}

Tuy nhiên, khi chạy chương trình kiểm thử, bạn sẽ thấy kiểm thử thất bại 😱

5755586203324389.png

Trong bước này, bạn sẽ tìm hiểu cách gỡ lỗi bằng cách sử dụng cây ngữ nghĩa.

Cây ngữ nghĩa

Chương trình kiểm thử Compose sử dụng một cấu trúc có tên là cây ngữ nghĩa (semantics tree) để tìm các thành phần trên màn hình và đọc thuộc tính của những thành phần này. Các dịch vụ hỗ trợ tiếp cận cũng sử dụng cấu trúc này để đọc các thuộc tính thông qua một dịch vụ, chẳng hạn như TalkBack.

Bạn có thể in cây ngữ nghĩa bằng cách sử dụng hàm printToLog trên một nút. Thêm dòng lệnh mới vào chương trình kiểm thử:

import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.printToLog
...

fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule.onRoot().printToLog("currentLabelExists")

    composeTestRule
        .onNodeWithText(RallyScreen.Accounts.name.uppercase())
        .assertExists() // Still fails
}

Bây giờ, hãy chạy chương trình kiểm thử và kiểm tra Logcat trong Android Studio (bạn có thể tìm currentLabelExists).

...com.example.compose.rally D/currentLabelExists: printToLog:
    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
     |-Node #2 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
       [SelectableGroup]
       MergeDescendants = 'true'
        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #11 at (l=552.0, t=105.0, r=615.0, b=168.0)px
          Role = 'Tab'
          Selected = 'false'
          StateDescription = 'Not selected'
          ContentDescription = 'Bills'
          Actions = [OnClick]
          MergeDescendants = 'true'
          ClearAndSetSemantics = 'true'

Khi xem cây ngữ nghĩa, bạn có thể thấy SelectableGroup có 3 phần tử con. Đó là các thẻ trong thanh ứng dụng ở trên cùng. Kết quả là không có thuộc tính text nào có giá trị "ACCOUNTS" và đây là lý do khiến chương trình kiểm thử thất bại. Tuy nhiên, mỗi thẻ đều có một phần mô tả nội dung riêng. Bạn có thể kiểm tra cách thiết lập thuộc tính này trong thành phần kết hợp RallyTab bên trong RallyTopAppBar.kt:

private fun RallyTab(text: String...)
...
    Modifier
        .clearAndSetSemantics { contentDescription = text }

Đối tượng sửa đổi này đang xoá các thuộc tính qua các thành phần con cháu và thiết lập phần mô tả nội dung của riêng mình. Đó là lý do bạn thấy "Accounts" (Tài khoản) thay vì "ACCOUNTS" (TÀI KHOẢN).

Hãy thay thế trình tìm kiếm onNodeWithText bằng onNodeWithContentDescription rồi chạy chương trình kiểm thử:

fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNodeWithContentDescription(RallyScreen.Accounts.name)
        .assertExists()
}

b5a7ae9f8f0ed750.png

Xin chúc mừng! Bạn đã sửa được chương trình kiểm thử và tìm hiểu về ComposeTestRule, kiểm thử riêng biệt, trình tìm kiếm, câu nhận định cũng như cách gỡ lỗi trong Cây ngữ nghĩa.

Tuy nhiên, tin xấu là: chương trình kiểm thử này vẫn chưa thực sự hữu ích lắm! Quan sát kỹ Cây ngữ nghĩa, bạn sẽ thấy phần mô tả nội dung của cả ba thẻ đều xuất hiện cho dù các thẻ đó có được chọn hay không. Chúng ta sẽ tìm hiểu kỹ hơn nhé!

5. Cây ngữ nghĩa hợp nhất và chưa hợp nhất

Cây ngữ nghĩa cần đảm bảo càng ngắn gọn càng tốt, chỉ cho thấy những thông tin có liên quan.

Ví dụ như trong TopAppBar, chúng ta không cần sử dụng nhiều nút riêng cho biểu tượng và nhãn. Hãy xem nút "Overview" (Tổng quan):

120e5327856286cd.png

        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'

Nút này có các thuộc tính (chẳng hạn như SelectedRole) được định nghĩa riêng cho thành phần selectable và một phần mô tả nội dung cho toàn bộ thẻ. Đây là những thuộc tính cấp cao, rất hữu ích cho các kiểm thử đơn giản. Thông tin chi tiết về biểu tượng hoặc văn bản sẽ không cần thiết nên không xuất hiện trên cây ngữ nghĩa.

Compose sẽ tự động cho thấy các thuộc tính Ngữ nghĩa này trong một số thành phần kết hợp, chẳng hạn như Text. Bạn cũng có thể tuỳ chỉnh và hợp nhất các thành phần đó để đại diện cho một thành phần duy nhất gồm một hoặc nhiều thành phần con cháu. Ví dụ: bạn có thể thể hiện một Button chứa thành phần kết hợp Text. Thuộc tính MergeDescendants = 'true' cho chúng ta biết rằng nút này có các thành phần con cháu, nhưng những thành phần này đã được hợp nhất vào nút này. Thông thường, trong các chương trình kiểm thử, chúng ta cần truy cập tất cả các nút.

Để xác định liệu thành phần kết hợp Text bên trong một thẻ có xuất hiện hay không, chúng ta có thể truy vấn Cây ngữ nghĩa chưa hợp nhất bằng cách truyền giá trị useUnmergedTree = true vào trình tìm kiếm onRoot.

@Test
fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule.onRoot(useUnmergedTree = true).printToLog("currentLabelExists")

}

Đầu ra trong Logcat bây giờ sẽ dài hơn một chút:

    Printing with useUnmergedTree = 'true'
    Node #1 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
     |-Node #2 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
       [SelectableGroup]
       MergeDescendants = 'true'
        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |  |-Node #9 at (l=284.0, t=105.0, r=468.0, b=154.0)px
        |    Text = 'ACCOUNTS'
        |    Actions = [GetTextLayoutResult]
        |-Node #11 at (l=552.0, t=105.0, r=615.0, b=168.0)px
          Role = 'Tab'
          Selected = 'false'
          StateDescription = 'Not selected'
          ContentDescription = 'Bills'
          Actions = [OnClick]
          MergeDescendants = 'true'
          ClearAndSetSemantics = 'true'

Nút #3 vẫn không có thành phần con cháu nào:

        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'

Nhưng nút 6 (thẻ được chọn) có một thành phần như vậy và bây giờ chúng ta đã thấy xuất hiện thuộc tính "Text" ("Văn bản"):

        |-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        |  |-Node #9 at (l=284.0, t=105.0, r=468.0, b=154.0)px
        |    Text = 'ACCOUNTS'
        |    Actions = [GetTextLayoutResult]

Để xác định xem hành vi này có đúng như kỳ vọng hay không, bạn sẽ viết một trình so khớp để tìm một nút chứa văn bản "ACCOUNTS" ("TÀI KHOẢN"), trong đó thành phần mẹ của nút này là một nút có phần mô tả nội dung là "Account" ("Tài khoản").

Hãy xem lại Bản tóm tắt về chương trình kiểm thử Compose và thử tìm cách viết trình so khớp đó. Hãy lưu ý bạn có thể sử dụng toán tử boolean như andor cho trình so khớp này.

Tất cả trình tìm kiếm đều có tham số tên là useUnmergedTree. Hãy đặt giá trị tham số này thành true để sử dụng cây ngữ nghĩa chưa hợp nhất.

Bạn hãy thử viết chương trình kiểm thử này mà không xem phần đáp án dưới đây!

Đáp án

import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasText
...

@Test
fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNode(
            hasText(RallyScreen.Accounts.name.uppercase()) and
            hasParent(
                hasContentDescription(RallyScreen.Accounts.name)
            ),
            useUnmergedTree = true
        )
        .assertExists()
}

Hãy tiếp tục và chạy chương trình kiểm thử:

94c57e2cfc12c10b.png

Xin chúc mừng! Trong bước này, bạn đã tìm hiểu về việc hợp nhất thuộc tính cũng như Cây ngữ nghĩa hợp nhất và chưa hợp nhất.

6. Đồng bộ hoá

Mọi chương trình kiểm thử bạn viết đều phải được đồng bộ hoá cho phù hợp với vấn đề được kiểm thử. Ví dụ: khi bạn sử dụng một trình tìm kiếm như onNodeWithText, chương trình kiểm thử sẽ chờ cho đến khi ứng dụng ở trạng thái rảnh (idle) trước khi truy vấn cây ngữ nghĩa. Nếu không đồng bộ hoá, chương trình kiểm thử có thể tìm kiếm các thành phần trước khi cho thấy những thành phần này hoặc có thể mất thời gian chờ đợi không cần thiết.

Ở bước này, chúng ta sẽ sử dụng màn hình Overview ("Tổng quan") có dạng như dưới đây khi bạn chạy ứng dụng:

8c467af3570b8de6.gif

Hãy lưu ý phần ảnh động nhấp nháy liên tục trên thẻ Alerts (Cảnh báo) (nhằm giúp thu hút sự chú ý đến phần tử này).

Tạo một lớp kiểm thử khác có tên là OverviewScreenTest rồi thêm nội dung sau:

package com.example.compose.rally

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import com.example.compose.rally.ui.overview.OverviewBody
import org.junit.Rule
import org.junit.Test

class OverviewScreenTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun overviewScreen_alertsDisplayed() {
        composeTestRule.setContent {
            OverviewBody()
        }

        composeTestRule
            .onNodeWithText("Alerts")
            .assertIsDisplayed()
    }
}

Nếu chạy chương trình kiểm thử này, bạn sẽ thấy nó không bao giờ kết thúc (hết giờ sau 30 giây).

b2d71bd417326bd3.png

Nội dung thông báo lỗi:

androidx.compose.ui.test.junit4.android.ComposeNotIdleException: Idling resource timed out: possibly due to compose being busy.
IdlingResourceRegistry has the following idling resources registered:
- [busy] androidx.compose.ui.test.junit4.android.ComposeIdlingResource@d075f91 

Về cơ bản, lỗi này cho bạn biết rằng Compose luôn bận và không thể đồng bộ hoá ứng dụng với chương trình kiểm thử này.

Bạn có thể đoán được nguyên nhân là do phần ảnh động nhấp nháy liên tục. Ứng dụng không bao giờ ở trạng thái rảnh và chương trình kiểm thử không thể tiếp tục.

Hãy tìm hiểu cách triển khai phần ảnh động vô hạn này:

app/src/main/java/com/example/compose/rally/ui/overview/OverviewBody.kt

var currentTargetElevation by remember {  mutableStateOf(1.dp) }
LaunchedEffect(Unit) {
    // Start the animation
    currentTargetElevation = 8.dp
}
val animatedElevation = animateDpAsState(
    targetValue = currentTargetElevation,
    animationSpec = tween(durationMillis = 500),
    finishedListener = {
        currentTargetElevation = if (currentTargetElevation > 4.dp) {
            1.dp
        } else {
            8.dp
        }
    }
)
Card(elevation = animatedElevation.value) { ... }

Về cơ bản, mã nguồn này đang chờ một ảnh động kết thúc (finishedListener) rồi chạy lại ảnh động đó.

Để khắc phục vấn đề này, có thể vô hiệu hoá ảnh động trong phần tuỳ chọn cho nhà phát triển. Đây là một trong những cách xử lý được chấp nhận rộng rãi khi dùng View.

Trong Compose, thiết kế của các API ảnh động có tính đến khả năng kiểm thử, vì vậy bạn có thể sử dụng API phù hợp để khắc phục vấn đề này. Thay vì khởi động lại ảnh động animateDpAsState, chúng ta có thể sử dụng ảnh động vô hạn.

Thay thế mã nguồn trong OverviewScreen bằng API thích hợp:

import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateValue
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.ui.unit.Dp
...

    val infiniteElevationAnimation = rememberInfiniteTransition()
    val animatedElevation: Dp by infiniteElevationAnimation.animateValue(
        initialValue = 1.dp,
        targetValue = 8.dp,
        typeConverter = Dp.VectorConverter,
        animationSpec = infiniteRepeatable(
            animation = tween(500),
            repeatMode = RepeatMode.Reverse
        )
    )
    Card(elevation = animatedElevation) {

Chạy lại chương trình kiểm thử, bạn sẽ thấy kết quả kiểm thử bây giờ đã đạt:

369e266eed40e4e4.png

Xin chúc mừng! Trong bước này, bạn đã tìm hiểu về việc đồng bộ hoá cũng như tác động của ảnh động đối với chương trình kiểm thử.

7. Bài tập không bắt buộc

Trong bước này, bạn sẽ sử dụng một thao tác (xem Bản tóm tắt về chương trình kiểm thử) để xác minh xem thao tác nhấp vào các thẻ của RallyTopAppBar có làm thay đổi lựa chọn hay không.

Gợi ý:

  • Cần đưa cả dữ liệu trạng thái (do RallyApp sở hữu) vào phạm vi kiểm thử.
  • Xác minh trạng thái, đừng xác minh hành vi. Sử dụng câu nhận định cho trạng thái giao diện người dùng thay vì dựa vào đối tượng được gọi và cách thức gọi đối tượng.

Chúng tôi không đưa ra đáp án cho bài tập này.

8. Các bước tiếp theo

Xin chúc mừng! Bạn đã hoàn tất bài học về Kiểm thử trong Jetpack Compose. Bây giờ, bạn đã có các khối xây dựng cơ bản để tạo chiến lược kiểm thử hiệu quả cho giao diện người dùng Compose.

Để tìm hiểu thêm về Kiểm thử và Compose, hãy tham khảo các tài nguyên sai đây:

  1. Tài liệu về chương trình kiểm thử có thêm thông tin về trình tìm kiếm, câu nhận định, thao tác, trình so khớp, cũng như cơ chế đồng bộ hoá, thao tác trên đối tượng thời gian, v.v.
  2. Hãy đánh dấu trang Bản tóm tắt về kiểm thử!
  3. Mẫu Rally đi kèm một lớp kiểm thử ảnh chụp màn hình đơn giản. Hãy khám phá tệp AnimatingCircleTests.kt để tìm hiểu thêm.
  4. Để xem hướng dẫn chung về cách kiểm thử ứng dụng Android, bạn có thể theo dõi 3 lớp học lập trình sau đây:
  1. Kho lưu trữ mẫu Compose trên GitHub gồm nhiều ứng dụng đã tạo sẵn kiểm thử giao diện người dùng.
  2. Lộ trình Jetpack Compose cung cấp danh sách tài nguyên giúp bạn bắt đầu tìm hiểu Compose.

Chúc bạn kiểm thử vui vẻ!