一、測(cè)試基礎(chǔ)知識(shí)
1.1 測(cè)試級(jí)別
測(cè)試金字塔(如圖 2 所示)說(shuō)明了應(yīng)用應(yīng)如何包含三類測(cè)試(即小型、中型和大型測(cè)試):
-
小型測(cè)試是指單元測(cè)試,用于驗(yàn)證應(yīng)用的行為,一次驗(yàn)證一個(gè)類。
-
中型測(cè)試是指集成測(cè)試,用于驗(yàn)證模塊內(nèi)堆棧級(jí)別之間的互動(dòng)或相關(guān)模塊之間的互動(dòng)。
-
大型測(cè)試是指端到端測(cè)試,用于驗(yàn)證跨越了應(yīng)用的多個(gè)模塊的用戶操作流程。
沿著金字塔逐級(jí)向上,從小型測(cè)試到大型測(cè)試,各類測(cè)試的保真度逐級(jí)提高,但維護(hù)和調(diào)試工作所需的執(zhí)行時(shí)間和工作量也逐級(jí)增加。因此,您編寫(xiě)的單元測(cè)試應(yīng)多于集成測(cè)試,集成測(cè)試應(yīng)多于端到端測(cè)試。雖然各類測(cè)試的比例可能會(huì)因應(yīng)用的用例不同而異,但我們通常建議各類測(cè)試所占比例如下:小型測(cè)試占 70%,中型測(cè)試占 20%,大型測(cè)試占 10%。
圖1 測(cè)試金字塔,顯示了應(yīng)用的測(cè)試套件應(yīng)包含的三類測(cè)試
1.2 迭代創(chuàng)建和測(cè)試代碼
迭代開(kāi)發(fā)某項(xiàng)功能時(shí),您可以先編寫(xiě)一個(gè)新測(cè)試,也可以將用例和斷言添加到現(xiàn)有單元測(cè)試。測(cè)試最初會(huì)失敗,因?yàn)樵摴δ苌形磳?shí)現(xiàn)。
務(wù)必考慮隨著設(shè)計(jì)新功能而出現(xiàn)的責(zé)任單元。對(duì)于每個(gè)單元,您需要編寫(xiě)相應(yīng)的單元測(cè)試。您的單元測(cè)試應(yīng)幾乎囊括與單元的所有可能的互動(dòng),包括標(biāo)準(zhǔn)互動(dòng)、無(wú)效輸入以及資源不可用的情況。應(yīng)盡可能利用 Jetpack 庫(kù);當(dāng)您使用這些經(jīng)過(guò)充分測(cè)試的庫(kù)時(shí),您可以專注于驗(yàn)證您的應(yīng)用特有的行為。
圖2 與由測(cè)試驅(qū)動(dòng)的迭代開(kāi)發(fā)關(guān)聯(lián)的兩個(gè)周期
完整的工作流(如圖 1 所示)包含一系列嵌套的迭代周期,其中一個(gè)由界面驅(qū)動(dòng)的漫長(zhǎng)而緩慢的周期用來(lái)測(cè)試代碼單元的集成。您可以使用更短且更快的開(kāi)發(fā)周期來(lái)測(cè)試單元本身。這一組周期一直持續(xù)到您的應(yīng)用滿足每個(gè)用例為止。
二、構(gòu)建單元測(cè)試
單元測(cè)試是應(yīng)用測(cè)試策略中的基本測(cè)試。通過(guò)針對(duì)代碼創(chuàng)建和運(yùn)行單元測(cè)試,您可以輕松驗(yàn)證各個(gè)單元的邏輯是否正確。在每次構(gòu)建后運(yùn)行單元測(cè)試可幫助您快速捕捉和修復(fù)由應(yīng)用的代碼更改導(dǎo)致的軟件回歸。
單元測(cè)試通常以可重復(fù)的方式運(yùn)用盡可能小的代碼單元(可能是方法、類或組件)的功能。當(dāng)您需要驗(yàn)證應(yīng)用中特定代碼的邏輯時(shí),應(yīng)構(gòu)建單元測(cè)試。例如,如果您正在對(duì)某個(gè)類進(jìn)行單元測(cè)試,測(cè)試可能會(huì)檢查該類是否處于正確狀態(tài)。通常,代碼單元在隔離的環(huán)境中進(jìn)行測(cè)試;您的測(cè)試僅影響和監(jiān)控對(duì)該單元的更改。您可以使用依賴項(xiàng)提供器(如 Robolectric)或模擬框架將您的單元與其依賴項(xiàng)隔離開(kāi)來(lái)。
2.1 構(gòu)建本地單元測(cè)試
設(shè)置測(cè)試環(huán)境
在 Android Studio 項(xiàng)目中,您必須將本地單元測(cè)試的源文件存儲(chǔ)在 module-name/src/test/java/
中。當(dāng)您創(chuàng)建新項(xiàng)目時(shí),此目錄已存在。
您還需要為項(xiàng)目配置測(cè)試依賴項(xiàng),以使用 JUnit 4 框架提供的標(biāo)準(zhǔn) API。如果您的測(cè)試需要與 Android 依賴項(xiàng)互動(dòng),請(qǐng)?zhí)砑?Robolectric 或 Mockito 庫(kù)以簡(jiǎn)化您的本地單元測(cè)試。
在應(yīng)用的頂級(jí) build.gradle
文件中,請(qǐng)將以下庫(kù)指定為依賴項(xiàng):
dependencies { // Required -- JUnit 4 framework testImplementation 'junit:junit:4.12' // Optional -- Robolectric environment testImplementation 'androidx.test:core:1.0.0' // Optional -- Mockito framework testImplementation 'org.mockito:mockito-core:1.10.19' }
創(chuàng)建本地單元測(cè)試類
本地單元測(cè)試類應(yīng)編寫(xiě)為 JUnit 4 測(cè)試類。JUnit 是最受歡迎且應(yīng)用最廣泛的 Java 單元測(cè)試框架。與原先的版本相比,JUnit 4 可讓您以更簡(jiǎn)潔且更靈活的方式編寫(xiě)測(cè)試,因?yàn)?JUnit 4 不要求您執(zhí)行以下操作:
-
擴(kuò)展
junit.framework.TestCase
類。 -
在測(cè)試方法名稱前面加上
'test'
關(guān)鍵字作為前綴。 -
使用
junit.framework
或junit.extensions
軟件包中的類。
如需創(chuàng)建基本的 JUnit 4 測(cè)試類,請(qǐng)創(chuàng)建包含一個(gè)或多個(gè)測(cè)試方法的類。測(cè)試方法以 @Test
注釋開(kāi)頭,并且包含用于運(yùn)用和驗(yàn)證要測(cè)試的組件中的單項(xiàng)功能的代碼。
以下示例展示了如何實(shí)現(xiàn)本地單元測(cè)試類。測(cè)試方法 emailValidator_CorrectEmailSimple_ReturnsTrue
驗(yàn)證被測(cè)應(yīng)用中的 isValidEmail()
方法是否返回正確的結(jié)果。
import com.google.common.truth.Truth.assertThat import org.junit.Test class EmailValidatorTest { @Test fun emailValidator_CorrectEmailSimple_ReturnsTrue() { assertThat(EmailValidator.isValidEmail("name@email.com")).isTrue() } }
如需創(chuàng)建容易讀懂的測(cè)試來(lái)評(píng)估應(yīng)用中的組件是否返回預(yù)期的結(jié)果,我們建議使用 Truth 庫(kù)和 Android Assertions 中的類,如前面的示例所示。如需詳細(xì)了解 Truth 和 Android Assertions 支持哪些類型的邏輯驗(yàn)證,請(qǐng)參閱介紹如何創(chuàng)建更容易讀懂的斷言的部分。
不過(guò),如果您更愿意使用 junit.Assert 方法或 Hamcrest 匹配器(如 is()
和 equalTo()
方法)來(lái)比較預(yù)期結(jié)果與實(shí)際結(jié)果,也可以改用這些庫(kù)。
添加框架依賴項(xiàng)
如果您的測(cè)試與多個(gè) Android 框架依賴項(xiàng)互動(dòng),或以復(fù)雜的方式與這些依賴項(xiàng)互動(dòng),請(qǐng)使用 AndroidX Test 提供的 Robolectric 工件。Robolectric 在本地 JVM 或真實(shí)設(shè)備上執(zhí)行真實(shí)的 Android 框架代碼和原生框架代碼的虛假對(duì)象。
以下示例展示了如何創(chuàng)建使用 Robolectric 的單元測(cè)試:
app/build.gradle
android { // ... testOptions { unitTests.includeAndroidResources = true } }
MyLocalUnitTestClass
import android.content.Context import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import org.junit.Test private const val FAKE_STRING = "HELLO_WORLD" class UnitTestSample { val context = ApplicationProvider.getApplicationContext<Context>() @Test fun readStringFromContext_LocalizedString() { // Given a Context object retrieved from Robolectric... val myObjectUnderTest = ClassUnderTest(context) // ...when the string is returned from the object under test... val result: String = myObjectUnderTest.getHelloWorldString() // ...then the result should be the expected one. assertThat(result).isEqualTo(FAKE_STRING) } }
添加模擬依賴項(xiàng)
默認(rèn)情況下,Android Plug-in for Gradle 針對(duì)一個(gè)修改版 android.jar
庫(kù)(不包含任何實(shí)際代碼)執(zhí)行本地單元測(cè)試。從單元測(cè)試對(duì) Android 類的方法調(diào)用會(huì)拋出異常。這是為了確保您僅測(cè)試代碼,而不依賴于 Android 平臺(tái)的任何特定行為,即您未明確構(gòu)建或模擬的行為。
模擬 Android 依賴項(xiàng)
如果您的測(cè)試對(duì) Android 的依賴性極小,并且您需要在應(yīng)用中測(cè)試組件與其依賴項(xiàng)之間的特定互動(dòng),請(qǐng)使用模擬框架對(duì)代碼中的外部依賴項(xiàng)打樁。這樣,您就可以輕松地測(cè)試組件是否按預(yù)期方式與依賴項(xiàng)互動(dòng)。通過(guò)用模擬對(duì)象代替 Android 依賴項(xiàng),您可以將單元測(cè)試與 Android 系統(tǒng)的其余部分隔離,同時(shí)驗(yàn)證是否調(diào)用了這些依賴項(xiàng)中的正確方法。適用于 Java(版本 1.9.5 及更高版本)的 Mockito 模擬框架提供了與 Android 單元測(cè)試的兼容性。通過(guò) Mockito,您可以將模擬對(duì)象配置為在被調(diào)用時(shí)返回某個(gè)特定值。
如需使用此框架將模擬對(duì)象添加到本地單元測(cè)試,請(qǐng)遵循以下編程模型:
-
在
build.gradle
文件中添加 Mockito 庫(kù)依賴項(xiàng),如設(shè)置測(cè)試環(huán)境中所述。 -
在單元測(cè)試類定義的開(kāi)頭,添加
@RunWith(MockitoJUnitRunner.class)
注釋。此注釋可告知 Mockito 測(cè)試運(yùn)行程序驗(yàn)證您對(duì)框架的使用是否正確無(wú)誤,并簡(jiǎn)化了模擬對(duì)象的初始化。 -
如需為 Android 依賴項(xiàng)創(chuàng)建模擬對(duì)象,請(qǐng)?jiān)谧侄温暶髑疤砑?
@Mock
注釋。 -
如需模擬依賴項(xiàng)的行為,您可以使用
when()
和thenReturn()
方法來(lái)指定某種條件以及滿足該條件時(shí)的返回值。
以下示例展示了如何創(chuàng)建使用模擬 Context
對(duì)象的單元測(cè)試。
import android.content.Context import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner private const val FAKE_STRING = "HELLO WORLD" @RunWith(MockitoJUnitRunner::class) class UnitTestSample { @Mock private lateinit var mockContext: Context @Test fun readStringFromContext_LocalizedString() { // Given a mocked Context injected into the object under test... `when`(mockContext.getString(R.string.hello_word)) .thenReturn(FAKE_STRING) val myObjectUnderTest = ClassUnderTest(mockContext) // ...when the string is returned from the object under test... val result: String = myObjectUnderTest.getHelloWorldString() // ...then the result should be the expected one. assertThat(result, `is`(FAKE_STRING)) } }
錯(cuò)誤:“Method ... not mocked”
如果您運(yùn)行的測(cè)試從并未模擬的 Android SDK 調(diào)用 API,您會(huì)收到一條錯(cuò)誤,指出未模擬此方法。這是因?yàn)?,用于運(yùn)行單元測(cè)試的 android.jar
文件不包含任何實(shí)際代碼(這些 API 僅由設(shè)備上的 Android 系統(tǒng)映像提供)。
默認(rèn)情況下,所有方法都會(huì)拋出異常。這是為了確保單元測(cè)試僅測(cè)試代碼,而不依賴于 Android 平臺(tái)的任何特定行為,即您未明確模擬(如使用 Mockito 模擬)的行為。
如果拋出的異常會(huì)給測(cè)試帶來(lái)問(wèn)題,您可以通過(guò)在項(xiàng)目的頂級(jí) build.gradle
文件中添加以下配置來(lái)更改行為,以使方法返回 null 或 0:
android { ... testOptions { unitTests.returnDefaultValues = true } }
2.2 構(gòu)建插樁單元測(cè)試
插樁單元測(cè)試是在實(shí)體設(shè)備和模擬器上運(yùn)行的測(cè)試,此類測(cè)試可以利用 Android 框架 API 和輔助性 API,如 AndroidX Test。插樁測(cè)試提供的保真度比本地單元測(cè)試要高,但運(yùn)行速度要慢得多。因此,我們建議只有在必須針對(duì)真實(shí)設(shè)備的行為進(jìn)行測(cè)試時(shí)才使用插樁單元測(cè)試。AndroidX Test 提供了幾個(gè)庫(kù),可讓您在必要時(shí)更輕松地編寫(xiě)插樁單元測(cè)試。例如,Android Builder 類可讓您更輕松地創(chuàng)建本來(lái)難以構(gòu)建的 Android 數(shù)據(jù)對(duì)象。
設(shè)置測(cè)試環(huán)境
在 Android Studio 項(xiàng)目中,您必須將插樁測(cè)試的源文件存儲(chǔ)在 module-name/src/androidTest/java/
中。此目錄在您創(chuàng)建新項(xiàng)目時(shí)已存在,并且包含一個(gè)插樁測(cè)試示例。
在開(kāi)始之前,您應(yīng)先添加 AndroidX Test API,以便為您的應(yīng)用快速構(gòu)建和運(yùn)行插樁測(cè)試代碼。AndroidX Test 包含 JUnit 4 測(cè)試運(yùn)行程序 (AndroidJUnitRunner
) 和用于功能界面測(cè)試的 API(Espresso 和 UI Automator)。
您還需要為項(xiàng)目配置 Android 測(cè)試依賴項(xiàng),以使用 AndroidX Test 提供的測(cè)試運(yùn)行程序和規(guī)則 API。為了簡(jiǎn)化測(cè)試開(kāi)發(fā),您還應(yīng)添加 Hamcrest 庫(kù),該庫(kù)可讓您使用 Hamcrest 匹配器 API 創(chuàng)建更靈活的斷言。
在應(yīng)用的頂級(jí) build.gradle
文件中,您需要將以下庫(kù)指定為依賴項(xiàng):
dependencies { androidTestImplementation 'androidx.test:runner:1.1.0' androidTestImplementation 'androidx.test:rules:1.1.0' // Optional -- Hamcrest library androidTestImplementation 'org.hamcrest:hamcrest-library:1.3' // Optional -- UI testing with Espresso androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' // Optional -- UI testing with UI Automator androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' }
如需使用 JUnit 4 測(cè)試類,請(qǐng)務(wù)必在您的項(xiàng)目中將 AndroidJUnitRunner
指定為默認(rèn)插樁測(cè)試運(yùn)行程序,方法是在應(yīng)用的模塊級(jí) build.gradle
文件中添加以下設(shè)置:
android { defaultConfig { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } }
創(chuàng)建插樁單元測(cè)試類
插樁單元測(cè)試類應(yīng)該是一個(gè) JUnit 4 測(cè)試類,它類似于有關(guān)如何創(chuàng)建本地單元測(cè)試類的部分中介紹的類。
如需創(chuàng)建 JUnit 4 插樁測(cè)試類,請(qǐng)將 AndroidJUnit4
指定為默認(rèn)測(cè)試運(yùn)行程序。
注意:如果您的測(cè)試套件依賴于 JUnit3 和 JUnit4 庫(kù)的混合搭配,請(qǐng)?jiān)跍y(cè)試類定義的開(kāi)頭添加 @RunWith(AndroidJUnit4::class)
注釋。
以下示例展示了如何編寫(xiě)插樁單元測(cè)試來(lái)驗(yàn)證是否為 LogHistory
類正確實(shí)現(xiàn)了 Parcelable
接口:
import android.os.Parcel import android.text.TextUtils.writeToParcel import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith const val TEST_STRING = "This is a string" const val TEST_LONG = 12345678L // @RunWith is required only if you use a mix of JUnit3 and JUnit4. @RunWith(AndroidJUnit4::class) @SmallTest class LogHistoryAndroidUnitTest { private lateinit var logHistory: LogHistory @Before fun createLogHistory() { logHistory = LogHistory() } @Test fun logHistory_ParcelableWriteRead() { val parcel = Parcel.obtain() logHistory.apply { // Set up the Parcelable object to send and receive. addEntry(TEST_STRING, TEST_LONG) // Write the data. writeToParcel(parcel, describeContents()) } // After you're done with writing, you need to reset the parcel for reading. parcel.setDataPosition(0) // Read the data. val createdFromParcel: LogHistory = LogHistory.CREATOR.createFromParcel(parcel) createdFromParcel.getData().also { createdFromParcelData: List<Pair<String, Long>> -> // Verify that the received data is correct. assertThat(createdFromParcelData.size).isEqualTo(1) assertThat(createdFromParcelData[0].first).isEqualTo(TEST_STRING) assertThat(createdFromParcelData[0].second).isEqualTo(TEST_LONG) } } }
創(chuàng)建測(cè)試套件
為了使插樁單元測(cè)試的執(zhí)行有條不紊,您可以將一系列測(cè)試類歸入一個(gè)測(cè)試套件類,并將這些測(cè)試一起運(yùn)行。測(cè)試套件可以嵌套;您的測(cè)試套件可以將其他測(cè)試套件歸在一起,并將其所有組件測(cè)試類一起運(yùn)行。
測(cè)試套件包含在測(cè)試軟件包中,類似于主應(yīng)用軟件包。按照慣例,測(cè)試套件軟件包名稱通常以 .suite
后綴結(jié)尾(例如,com.example.android.testing.mysample.suite
)。
如需為您的單元測(cè)試創(chuàng)建測(cè)試套件,請(qǐng)導(dǎo)入 JUnit RunWith
和 Suite
類。在您的測(cè)試套件中,添加 @RunWith(Suite.class)
和 @Suite.SuitClasses()
注釋。在 @Suite.SuiteClasses()
注釋中,將各個(gè)測(cè)試類或測(cè)試套件作為參數(shù)列出。
以下示例展示了如何實(shí)現(xiàn)名為UnitTestSuite
的測(cè)試套件,該測(cè)試套件將CalculatorInstrumentationTest
和CalculatorAddParameterizedTest
測(cè)試類組合在一起并運(yùn)行。
import com.example.android.testing.mysample.CalculatorAddParameterizedTest import com.example.android.testing.mysample.CalculatorInstrumentationTest import org.junit.runner.RunWith import org.junit.runners.Suite // Runs all unit tests. @RunWith(Suite::class) @Suite.SuiteClasses(CalculatorInstrumentationTest::class, CalculatorAddParameterizedTest::class) class UnitTestSuite
利用 Firebase 測(cè)試實(shí)驗(yàn)室運(yùn)行測(cè)試
使用 Firebase 測(cè)試實(shí)驗(yàn)室,您可以同時(shí)在許多主流 Android 設(shè)備和設(shè)備配置(語(yǔ)言區(qū)域、屏幕方向、屏幕尺寸和平臺(tái)版本)上測(cè)試您的應(yīng)用。這些測(cè)試在遠(yuǎn)程 Google 數(shù)據(jù)中心的物理設(shè)備和虛擬設(shè)備上運(yùn)行。您可以直接通過(guò) Android Studio 或從命令行將應(yīng)用部署到測(cè)試實(shí)驗(yàn)室。測(cè)試結(jié)果會(huì)提供測(cè)試日志,并包含所有應(yīng)用故障的詳細(xì)信息。
在開(kāi)始使用 Firebase 測(cè)試實(shí)驗(yàn)室之前,除非您已經(jīng)擁有 Google 帳號(hào)和 Firebase 項(xiàng)目,否則您需要執(zhí)行以下操作:
-
創(chuàng)建一個(gè) Google 帳號(hào)(如果您還沒(méi)有帳號(hào))。
-
在 Firebase 控制臺(tái)中,點(diǎn)擊新建項(xiàng)目。
-
利用測(cè)試實(shí)驗(yàn)室在 Spark 方案每日免費(fèi)配額內(nèi)測(cè)試您的應(yīng)用時(shí)不收取任何費(fèi)用。
三、自動(dòng)執(zhí)行界面測(cè)試
通過(guò)界面測(cè)試,您可以確保應(yīng)用滿足其功能要求并達(dá)到較高的質(zhì)量標(biāo)準(zhǔn),從而更有可能成功地被用戶采用。
界面測(cè)試的一種方法是直接讓測(cè)試人員對(duì)目標(biāo)應(yīng)用執(zhí)行一系列用戶操作,并驗(yàn)證其行為是否正常。不過(guò),這種人工方法會(huì)非常耗時(shí)、繁瑣且容易出錯(cuò)。一種更高效的方法是編寫(xiě)界面測(cè)試,以便以自動(dòng)化方式執(zhí)行用戶操作。自動(dòng)化方法可讓您以可重復(fù)的方式快速可靠地運(yùn)行測(cè)試。
如需使用 Android Studio 自動(dòng)執(zhí)行界面測(cè)試,請(qǐng)?jiān)趩为?dú)的 Android 測(cè)試文件夾 (src/androidTest/java
) 中實(shí)現(xiàn)測(cè)試代碼。Android Plugin for Gradle 會(huì)根據(jù)測(cè)試代碼構(gòu)建一個(gè)測(cè)試應(yīng)用,然后在目標(biāo)應(yīng)用所在的設(shè)備上加載該測(cè)試應(yīng)用。在測(cè)試代碼中,您可以使用界面測(cè)試框架來(lái)模擬目標(biāo)應(yīng)用上的用戶交互,以便執(zhí)行涵蓋特定使用場(chǎng)景的測(cè)試任務(wù)。
為了測(cè)試 Android 應(yīng)用,您通常會(huì)創(chuàng)建下面這些類型的自動(dòng)化界面測(cè)試:
-
涵蓋單個(gè)應(yīng)用的界面測(cè)試:這種類型的測(cè)試可驗(yàn)證目標(biāo)應(yīng)用在用戶執(zhí)行特定操作或在其 Activity 中輸入特定內(nèi)容時(shí)的行為是否符合預(yù)期。它可讓您檢查目標(biāo)應(yīng)用是否返回正確的界面輸出來(lái)響應(yīng)應(yīng)用 Activity 中的用戶交互。諸如 Espresso 之類的界面測(cè)試框架可讓您以編程方式模擬用戶操作,并測(cè)試復(fù)雜的應(yīng)用內(nèi)用戶交互。
-
涵蓋多個(gè)應(yīng)用的界面測(cè)試:這種類型的測(cè)試可驗(yàn)證不同用戶應(yīng)用之間交互或用戶應(yīng)用與系統(tǒng)應(yīng)用之間交互的正確行為。例如,您可能想要測(cè)試相機(jī)應(yīng)用是否能夠與第三方社交媒體應(yīng)用或默認(rèn)的 Android 相冊(cè)應(yīng)用正確分享圖片。支持跨應(yīng)用交互的界面測(cè)試框架(如 UI Automator)可讓您針對(duì)此類場(chǎng)景創(chuàng)建測(cè)試。
測(cè)試單個(gè)應(yīng)用的界面
測(cè)試單個(gè)應(yīng)用內(nèi)的用戶交互有助于確保用戶在與應(yīng)用交互時(shí)不會(huì)遇到意外結(jié)果或體驗(yàn)不佳的情況。如果您需要驗(yàn)證應(yīng)用的界面是否正常運(yùn)行,應(yīng)養(yǎng)成創(chuàng)建界面測(cè)試的習(xí)慣。
由 AndroidX Test 提供的 Espresso 測(cè)試框架提供了一些 API,用于編寫(xiě)界面測(cè)試以模擬單個(gè)目標(biāo)應(yīng)用內(nèi)的用戶交互。Espresso 測(cè)試可以在搭載 Android 2.3.3(API 級(jí)別 10)及更高版本的設(shè)備上運(yùn)行。使用 Espresso 的主要好處在于,它可以自動(dòng)同步測(cè)試操作與您正在測(cè)試的應(yīng)用的界面。Espresso 會(huì)檢測(cè)主線程何時(shí)處于空閑狀態(tài),以便可以在適當(dāng)?shù)臅r(shí)間運(yùn)行測(cè)試命令,從而提高測(cè)試的可靠性。此外,借助該功能,您不必在測(cè)試代碼中添加任何計(jì)時(shí)解決方法,如 Thread.sleep()
。
Espresso 測(cè)試框架是基于插樁的 API,可與 AndroidJUnitRunner
測(cè)試運(yùn)行程序一起使用
設(shè)置 Espresso
在使用 Espresso 構(gòu)建界面測(cè)試之前,請(qǐng)務(wù)必設(shè)置對(duì) Espresso 庫(kù)的依賴項(xiàng)引用:
dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' }
在測(cè)試設(shè)備上關(guān)閉動(dòng)畫(huà) - 如果讓系統(tǒng)動(dòng)畫(huà)在測(cè)試設(shè)備上保持開(kāi)啟狀態(tài),可能會(huì)導(dǎo)致意外結(jié)果或?qū)е聹y(cè)試失敗。通過(guò)以下方式關(guān)閉動(dòng)畫(huà):在“設(shè)置”中打開(kāi)“開(kāi)發(fā)者選項(xiàng)”,然后關(guān)閉以下所有選項(xiàng):
-
窗口動(dòng)畫(huà)縮放
-
過(guò)渡動(dòng)畫(huà)縮放
-
Animator 時(shí)長(zhǎng)縮放
創(chuàng)建 Espresso 測(cè)試類、
如需創(chuàng)建 Espresso 測(cè)試,請(qǐng)遵循以下編程模型:
-
通過(guò)調(diào)用
onView()
方法或AdapterView
控件的onData()
方法,在Activity
中找到要測(cè)試的界面組件(例如,應(yīng)用中的登錄按鈕)。 -
通過(guò)調(diào)用
ViewInteraction.perform()
或DataInteraction.perform()
方法并傳入用戶操作(例如,點(diǎn)擊登錄按鈕),模擬要在該界面組件上執(zhí)行的特定用戶交互。如需對(duì)同一界面組件上的多項(xiàng)操作進(jìn)行排序,請(qǐng)?jiān)诜椒▍?shù)中使用逗號(hào)分隔列表將它們鏈接起來(lái)。 -
根據(jù)需要重復(fù)上述步驟,以模擬目標(biāo)應(yīng)用中跨多個(gè) Activity 的用戶流。
-
執(zhí)行這些用戶交互后,使用
ViewAssertions
方法檢查界面是否反映了預(yù)期的狀態(tài)或行為。
下面幾部分更詳細(xì)地介紹了這些步驟。
以下代碼段展示了測(cè)試類如何調(diào)用此基本工作流程:
onView(withId(R.id.my_view)) // withId(R.id.my_view) is a ViewMatcher .perform(click()) // click() is a ViewAction .check(matches(isDisplayed())) // matches(isDisplayed()) is a ViewAssertion
將 Espresso 與 ActivityTestRule 一起使用
下文介紹如何創(chuàng)建新的 JUnit 4 型 Espresso 測(cè)試,并使用 ActivityTestRule
減少您需要編寫(xiě)的樣板代碼量。通過(guò)使用 ActivityTestRule
,測(cè)試框架會(huì)在帶有 @Test
注釋的每個(gè)測(cè)試方法運(yùn)行之前以及帶有 @Before
注釋的所有方法運(yùn)行之前啟動(dòng)被測(cè) Activity。該框架將在測(cè)試完成并且?guī)в?@After
注釋的所有方法都運(yùn)行后關(guān)閉該 Activity。
package com.example.android.testing.espresso.BasicSample import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import androidx.test.rule.ActivityTestRule import androidx.test.runner.AndroidJUnit4 @RunWith(AndroidJUnit4::class) @LargeTest class ChangeTextBehaviorTest { private lateinit var stringToBetyped: String @get:Rule var activityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) @Before fun initValidString() { // Specify a valid string. stringToBetyped = "Espresso" } @Test fun changeText_sameActivity() { // Type text and then press the button. onView(withId(R.id.editTextUserInput)) .perform(typeText(stringToBetyped), closeSoftKeyboard()) onView(withId(R.id.changeTextBt)).perform(click()) // Check that the text was changed. onView(withId(R.id.textToBeChanged)) .check(matches(withText(stringToBetyped))) } }
訪問(wèn)界面組件
您必須先指定界面組件或視圖,然后 Espresso 才能與被測(cè)應(yīng)用進(jìn)行交互。Espresso 支持使用 Hamcrest 匹配器指定應(yīng)用中的視圖和適配器。
如需查看視圖,請(qǐng)調(diào)用 onView()
方法并傳入用于指定目標(biāo)視圖的視圖匹配器。指定視圖匹配器部分對(duì)此進(jìn)行了更詳細(xì)的說(shuō)明。onView()
方法將返回一個(gè) ViewInteraction
對(duì)象,該對(duì)象允許測(cè)試與視圖進(jìn)行交互。但是,如果希望在 RecyclerView
布局中查找視圖,調(diào)用 onView()
方法可能不起作用。在這種情況下,請(qǐng)按照在 AdapterView 中查找視圖中的說(shuō)明進(jìn)行操作。
以下代碼段展示了如何編寫(xiě)一個(gè)先訪問(wèn) EditText
字段,再輸入文本字符串,接著關(guān)閉虛擬鍵盤(pán),然后執(zhí)行按鈕點(diǎn)擊操作的測(cè)試。
fun testChangeText_sameActivity() { // Type text and then press the button. onView(withId(R.id.editTextUserInput)) .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard()) onView(withId(R.id.changeTextButton)).perform(click()) // Check that the text was changed. ... }
指定視圖匹配器
您可以使用以下方法指定視圖匹配器:
-
調(diào)用
ViewMatchers
類中的方法。例如,如需通過(guò)查找視圖顯示的文本字符串查找視圖,您可以調(diào)用以下方法:
onView(withText("Sign-in"))
同樣,您可以調(diào)用 withId()
并提供視圖的資源 ID (R.id
),如以下示例所示:
onView(withId(R.id.button_signin))
-
不能保證 Android 資源 ID 是唯一的。如果測(cè)試嘗試匹配由多個(gè)視圖使用的某個(gè)資源 ID,Espresso 會(huì)拋出
AmbiguousViewMatcherException
。 -
使用
Matchers
類。您可以使用allOf()
方法組合多個(gè)匹配器,例如containsString()
和instanceOf()
。此方法可讓您更精細(xì)地過(guò)濾匹配結(jié)果,如以下示例所示:
onView(allOf(withId(R.id.button_signin), withText("Sign-in")))
您可以使用 not
關(guān)鍵字過(guò)濾與匹配器不對(duì)應(yīng)的視圖,如以下示例所示:
onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))))
如需在測(cè)試中使用這些方法,請(qǐng)導(dǎo)入 org.hamcrest.Matchers
軟件包。如需詳細(xì)了解 Hamcrest 匹配,請(qǐng)?jiān)L問(wèn) Hamcrest 網(wǎng)站。
如需提高 Espresso 測(cè)試的性能,請(qǐng)指定查找目標(biāo)視圖所需的最少匹配信息。例如,如果某個(gè)視圖可通過(guò)其描述性文本進(jìn)行唯一標(biāo)識(shí),您無(wú)需指定該視圖也可從 TextView
實(shí)例分配。
在 AdapterView 中查找視圖
在 AdapterView
微件中,視圖會(huì)在運(yùn)行時(shí)由子視圖動(dòng)態(tài)填充。如果您要測(cè)試的目標(biāo)視圖位于 AdapterView
(例如 ListView
、GridView
或 Spinner
)內(nèi),則 onView()
方法可能不起作用,因?yàn)橹荒軐⒁徊糠忠晥D加載到當(dāng)前視圖層次結(jié)構(gòu)中。
應(yīng)改為調(diào)用 onData()
方法獲取 DataInteraction
對(duì)象,以訪問(wèn)目標(biāo)視圖元素。Espresso 負(fù)責(zé)將目標(biāo)視圖元素加載到當(dāng)前視圖層次結(jié)構(gòu)中。Espresso 還負(fù)責(zé)滾動(dòng)到目標(biāo)元素,并將該元素置于焦點(diǎn)上。
注意:onData()
方法不檢查您指定的項(xiàng)是否與視圖對(duì)應(yīng)。Espresso 僅搜索當(dāng)前視圖層次結(jié)構(gòu)。如果未找到匹配項(xiàng),該方法會(huì)拋出 NoMatchingViewException
。
以下代碼段展示了如何結(jié)合使用 onData()
方法和 Hamcrest 匹配搜索列表中包含給定字符串的特定行。在本例中,LongListActivity
類包含通過(guò) SimpleAdapter
公開(kāi)的字符串列表。
onData(allOf(`is`(instanceOf(Map::class.java)), hasEntry(equalTo(LongListActivity.ROW_TEXT), `is`("test input"))))
執(zhí)行操作
調(diào)用 ViewInteraction.perform()
或 DataInteraction.perform()
方法,以模擬界面組件上的用戶交互。您必須將一個(gè)或多個(gè) ViewAction
對(duì)象作為參數(shù)傳入。Espresso 將按照給定的順序依次觸發(fā)每項(xiàng)操作,并在主線程中執(zhí)行這些操作。
ViewActions
類提供了用于指定常見(jiàn)操作的輔助程序方法的列表。您可以將這些方法用作方便的快捷方式,而不是創(chuàng)建和配置單個(gè) ViewAction
對(duì)象。您可以指定以下操作:
-
ViewActions.click()
:點(diǎn)擊視圖。 -
ViewActions.typeText()
:點(diǎn)擊視圖并輸入指定的字符串。 -
ViewActions.scrollTo()
:滾動(dòng)到視圖。目標(biāo)視圖必須是由ScrollView
派生的子類,并且其android:visibility
屬性的值必須為VISIBLE
。對(duì)于擴(kuò)展AdapterView
的視圖(例如ListView
),onData()
方法將負(fù)責(zé)為您滾動(dòng)。 -
ViewActions.pressKey()
:使用指定的鍵碼執(zhí)行按鍵操作。 -
ViewActions.clearText()
:清除目標(biāo)視圖中的文本。
如果目標(biāo)視圖位于 ScrollView
內(nèi),請(qǐng)先執(zhí)行 ViewActions.scrollTo()
操作以在屏幕中顯示該視圖,然后再繼續(xù)執(zhí)行其他操作。如果已顯示該視圖,則 ViewActions.scrollTo()
操作將不起作用。
使用 Espresso Intent 單獨(dú)測(cè)試 Activity
Espresso Intent 支持對(duì)應(yīng)用發(fā)出的 intent 進(jìn)行驗(yàn)證和打樁。使用 Espresso Intent,您可以通過(guò)以下方式單獨(dú)測(cè)試應(yīng)用、Activity 或服務(wù):攔截傳出 intent,對(duì)結(jié)果進(jìn)行打樁,然后將其發(fā)送回被測(cè)組件。
如需開(kāi)始使用 Espresso Intent 進(jìn)行測(cè)試,您需要將以下代碼行添加到應(yīng)用的 build.gradle 文件中:
dependencies { androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0' }
如需測(cè)試 intent,您需要?jiǎng)?chuàng)建 IntentsTestRule 類(與 ActivityTestRule 類非常相似)的實(shí)例。IntentsTestRule 類會(huì)在每次測(cè)試前初始化 Espresso Intent,終止托管 Activity,并在每次測(cè)試后釋放 Espresso Intent。
以下代碼段中顯示的測(cè)試類提供了顯式 intent 的簡(jiǎn)單測(cè)試。
private const val MESSAGE = "This is a test" private const val PACKAGE_NAME = "com.example.myfirstapp" @RunWith(AndroidJUnit4::class) class SimpleIntentTest { /* Instantiate an IntentsTestRule object. */ @get:Rule var intentsRule: IntentsTestRule<MainActivity> = IntentsTestRule(MainActivity::class.java) @Test fun verifyMessageSentToMessageActivity() { // Types a message into a EditText element. onView(withId(R.id.edit_message)) .perform(typeText(MESSAGE), closeSoftKeyboard()) // Clicks a button to send the message to another // activity through an explicit intent. onView(withId(R.id.send_message)).perform(click()) // Verifies that the DisplayMessageActivity received an intent // with the correct package name and message. intended(allOf( hasComponent(hasShortClassName(".DisplayMessageActivity")), toPackage(PACKAGE_NAME), hasExtra(MainActivity.EXTRA_MESSAGE, MESSAGE))) } }
使用 Espresso Web 測(cè)試 WebView
使用 Espresso Web,您可以測(cè)試包含在 Activity 中的 WebView
組件。它使用 WebDriver API 檢查和控制 WebView
的行為。
如需開(kāi)始使用 Espresso Web 進(jìn)行測(cè)試,您需要將以下代碼行添加到應(yīng)用的 build.gradle 文件中:
dependencies { androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.0' }
在使用 Espresso Web 創(chuàng)建測(cè)試的過(guò)程中,當(dāng)您實(shí)例化 ActivityTestRule 對(duì)象以測(cè)試 Activity 時(shí),需要在 WebView
上啟用 JavaScript。在測(cè)試中,您可以選擇 WebView
中顯示的 HTML 元素并模擬用戶交互,例如在文本框中輸入文本,然后點(diǎn)擊某個(gè)按鈕。完成這些操作后,您可以驗(yàn)證網(wǎng)頁(yè)上的結(jié)果是否與預(yù)期結(jié)果一致。
以下代碼段中,該類測(cè)試被測(cè) Activity 中 ID 值為“webview”的 WebView
組件。typeTextInInput_clickButton_SubmitsForm()
測(cè)試先選擇網(wǎng)頁(yè)上的一個(gè) <input>
元素,再輸入一些文本,然后檢查出現(xiàn)在另一個(gè)元素中的文本。
private const val MACCHIATO = "Macchiato" private const val DOPPIO = "Doppio" @LargeTest @RunWith(AndroidJUnit4::class) class WebViewActivityTest { @get:Rule val activityRule = object : ActivityTestRule<WebViewActivity>( WebViewActivity::class.java, false, /* Initial touch mode */ false /* launch activity */ ) { override fun afterActivityLaunched() { // Enable JavaScript. onWebView().forceJavascriptEnabled() } } @Test fun typeTextInInput_clickButton_SubmitsForm() { // Lazily launch the Activity with a custom start Intent per test activityRule.launchActivity(withWebFormIntent()) // Selects the WebView in your layout. // If you have multiple WebViews you can also use a // matcher to select a given WebView, onWebView(withId(R.id.web_view)). onWebView() // Find the input element by ID .withElement(findElement(Locator.ID, "text_input")) // Clear previous input .perform(clearElement()) // Enter text into the input element .perform(DriverAtoms.webKeys(MACCHIATO)) // Find the submit button .withElement(findElement(Locator.ID, "submitBtn")) // Simulate a click via JavaScript .perform(webClick()) // Find the response element by ID .withElement(findElement(Locator.ID, "response")) // Verify that the response page contains the entered text .check(webMatches(getText(), containsString(MACCHIATO))) } }
驗(yàn)證結(jié)果
調(diào)用 ViewInteraction.check()
或 DataInteraction.check()
方法以斷言界面中的視圖與某種預(yù)期狀態(tài)匹配。您必須將 ViewAssertion
對(duì)象作為參數(shù)傳入。如果斷言失敗,Espresso 會(huì)拋出 AssertionFailedError
。
ViewAssertions
類提供了用于指定常見(jiàn)斷言的輔助程序方法的列表??梢允褂玫臄嘌园ǎ?/p>
-
doesNotExist
:斷言當(dāng)前視圖層次結(jié)構(gòu)中沒(méi)有符合指定條件的視圖。 -
matches
:斷言當(dāng)前視圖層次結(jié)構(gòu)中存在指定的視圖,并且其狀態(tài)與某個(gè)給定的 Hamcrst 匹配器匹配。 -
selectedDescendentsMatch
:斷言存在父視圖的指定子視圖,并且其狀態(tài)與某個(gè)給定的 Hamcrst 匹配器匹配。
以下代碼段展示了如何檢查界面中顯示的文本與先前在 EditText
字段中輸入的文本是否具有相同的值。
fun testChangeText_sameActivity() { // Type text and then press the button. ... // Check that the text was changed. onView(withId(R.id.textToBeChanged)) .check(matches(withText(STRING_TO_BE_TYPED))) }
測(cè)試多個(gè)應(yīng)用的界面
通過(guò)涉及多個(gè)應(yīng)用中的用戶交互的界面測(cè)試,您可以驗(yàn)證當(dāng)用戶流跨入其他應(yīng)用或系統(tǒng)界面時(shí),您的應(yīng)用是否能夠正常運(yùn)行。短信應(yīng)用就是此類用戶流的一個(gè)例子,該應(yīng)用先讓用戶輸入短信,再啟動(dòng) Android 聯(lián)系人選擇器,以便用戶可以選擇短信的收件人,然后將控制權(quán)返還給原來(lái)的應(yīng)用,以便用戶提交短信。
本課介紹如何使用 AndroidX Test 提供的 UI Automator 測(cè)試框架來(lái)編寫(xiě)此類界面測(cè)試。通過(guò) UI Automator API,您可以與設(shè)備上的可見(jiàn)元素進(jìn)行交互,而不管焦點(diǎn)在哪個(gè) Activity
上。您的測(cè)試可以使用方便的描述符(如顯示在相應(yīng)組件中的文本或其內(nèi)容描述)來(lái)查找界面組件。UI Automator 測(cè)試可以在搭載 Android 4.3(API 級(jí)別 18)或更高版本的設(shè)備上運(yùn)行。
UI Automator 測(cè)試框架是基于插樁的 API,可與 AndroidJUnitRunner
測(cè)試運(yùn)行程序一起使用。
設(shè)置 UI Automator
在使用 UI Automator 構(gòu)建界面測(cè)試之前,請(qǐng)務(wù)必配置測(cè)試源代碼位置和項(xiàng)目依賴項(xiàng),如針對(duì) AndroidX Test 設(shè)置項(xiàng)目中所述。
在 Android 應(yīng)用模塊的 build.gradle
文件中,您必須設(shè)置對(duì) UI Automator 庫(kù)的依賴項(xiàng)引用:
dependencies { ... androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' }
要優(yōu)化 UI Automator 測(cè)試,您應(yīng)先檢查目標(biāo)應(yīng)用的界面組件并確保它們可訪問(wèn)。這些優(yōu)化提示將在接下來(lái)的兩部分中進(jìn)行介紹。
創(chuàng)建 UI Automator 測(cè)試類
UI Automator 測(cè)試類的編寫(xiě)方式應(yīng)與 JUnit 4 測(cè)試類相同。
在測(cè)試類定義的開(kāi)頭添加 @RunWith(AndroidJUnit4.class)
注釋。您還需要將 AndroidX Test 中提供的 AndroidJUnitRunner
類指定為默認(rèn)測(cè)試運(yùn)行程序。
在 UI Automator 測(cè)試類中實(shí)現(xiàn)以下編程模型:
-
通過(guò)調(diào)用
getInstance()
方法并將Instrumentation
對(duì)象作為參數(shù)傳遞給該方法,獲取UiDevice
對(duì)象以訪問(wèn)要測(cè)試的設(shè)備。 -
通過(guò)調(diào)用
findObject()
方法,獲取UiObject
對(duì)象以訪問(wèn)設(shè)備上顯示的界面組件(例如,前臺(tái)的當(dāng)前視圖)。 -
通過(guò)調(diào)用
UiObject
方法,模擬需要在該界面組件上執(zhí)行的特定用戶交互;例如,調(diào)用performMultiPointerGesture()
以模擬多點(diǎn)觸控手勢(shì),以及調(diào)用setText()
以修改文本字段。您可以根據(jù)需要反復(fù)調(diào)用第 2 步和第 3 步中的 API,以測(cè)試涉及多個(gè)界面組件或用戶操作序列的更復(fù)雜的用戶交互。 -
執(zhí)行這些用戶交互后,檢查界面是否反映了預(yù)期的狀態(tài)或行為。
下面幾部分更詳細(xì)地介紹了這些步驟。
訪問(wèn)界面組件
UiDevice
對(duì)象是您訪問(wèn)和操縱設(shè)備狀態(tài)的主要方式。在測(cè)試中,您可以調(diào)用 UiDevice
方法檢查各種屬性的狀態(tài),如當(dāng)前屏幕方向或顯示屏尺寸。您的測(cè)試可以使用 UiDevice
對(duì)象執(zhí)行設(shè)備級(jí)操作,如強(qiáng)制設(shè)備進(jìn)行特定旋轉(zhuǎn)、按方向鍵硬件按鈕,以及按主屏幕和菜單按鈕。
最好從設(shè)備的主屏幕開(kāi)始測(cè)試。在主屏幕(或您在設(shè)備中選擇的其他某個(gè)起始位置)上,您可以調(diào)用 UI Automator API 提供的方法,以選擇特定的界面元素并與之交互。
以下代碼段展示了您的測(cè)試如何獲取 UiDevice
實(shí)例并模擬按主屏幕按鈕的操作:
import org.junit.Before import androidx.test.runner.AndroidJUnit4 import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until ... private const val BASIC_SAMPLE_PACKAGE = "com.example.android.testing.uiautomator.BasicSample" private const val LAUNCH_TIMEOUT = 5000L private const val STRING_TO_BE_TYPED = "UiAutomator" @RunWith(AndroidJUnit4::class) @SdkSuppress(minSdkVersion = 18) class ChangeTextBehaviorTest2 { private lateinit var device: UiDevice @Before fun startMainActivityFromHomeScreen() { // Initialize UiDevice instance device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) // Start from the home screen device.pressHome() // Wait for launcher val launcherPackage: String = device.launcherPackageName assertThat(launcherPackage, notNullValue()) device.wait( Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT ) // Launch the app val context = ApplicationProvider.getApplicationContext<Context>() val intent = context.packageManager.getLaunchIntentForPackage( BASIC_SAMPLE_PACKAGE).apply { // Clear out any previous instances addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) } context.startActivity(intent) // Wait for the app to appear device.wait( Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), LAUNCH_TIMEOUT ) } }
使用 findObject()
方法檢索 UiObject
,它表示符合給定選擇器條件的視圖。您可以根據(jù)需要重復(fù)使用已在應(yīng)用測(cè)試的其他部分中創(chuàng)建的 UiObject
實(shí)例。請(qǐng)注意,每當(dāng)您的測(cè)試使用 UiObject
實(shí)例以點(diǎn)擊界面元素或查詢屬性時(shí),UI Automator 測(cè)試框架都會(huì)在當(dāng)前顯示內(nèi)容中搜索匹配項(xiàng)。
以下代碼段展示了您的測(cè)試如何構(gòu)建表示應(yīng)用中的“取消”按鈕和“確定”按鈕的 UiObject
實(shí)例。
val cancelButton: UiObject = device.findObject( UiSelector().text("Cancel").className("android.widget.Button") ) val okButton: UiObject = device.findObject( UiSelector().text("OK").className("android.widget.Button") ) // Simulate a user-click on the OK button, if found. if (okButton.exists() && okButton.isEnabled) { okButton.click() }
指定選擇器
如果您需要訪問(wèn)應(yīng)用中的特定界面組件,請(qǐng)使用 UiSelector
類。此類表示對(duì)當(dāng)前顯示的界面中特定元素的查詢。
如果找到了多個(gè)匹配元素,系統(tǒng)會(huì)將布局層次結(jié)構(gòu)中的第一個(gè)匹配元素作為目標(biāo) UiObject
返回。構(gòu)建 UiSelector
時(shí),您可以將多個(gè)屬性鏈接在一起以優(yōu)化搜索。如果未找到匹配的界面元素,系統(tǒng)會(huì)拋出 UiAutomatorObjectNotFoundException
。
您可以使用 childSelector()
方法來(lái)嵌套多個(gè) UiSelector
個(gè)實(shí)例。例如,以下代碼示例展示了您的測(cè)試如何指定搜索,以在當(dāng)前顯示的界面中查找第一個(gè) ListView
,然后在該 ListView
中搜索,以查找具有文本屬性“Apps”的界面元素。
val appItem: UiObject = device.findObject( UiSelector().className("android.widget.ListView") .instance(0) .childSelector( UiSelector().text("Apps") ) )
最佳做法是,在指定選擇器時(shí),應(yīng)使用資源 ID(如果已將其分配給界面元素),而不是文本元素或內(nèi)容描述符。并非所有元素都有文本元素(例如,工具欄中的圖標(biāo))。文本選擇器很脆弱,如果界面發(fā)生細(xì)微更改,可能會(huì)導(dǎo)致測(cè)試失敗。此外,文本選擇器也可能無(wú)法在不同語(yǔ)言之間擴(kuò)展,它們可能與翻譯的字符串不匹配。
在選擇器條件中指定對(duì)象狀態(tài)可能很有用。例如,如果要選擇所有已選中元素的列表以便取消選中這些元素,請(qǐng)調(diào)用 checked()
方法并將參數(shù)設(shè)置為 true
。
執(zhí)行操作
您的測(cè)試獲取 UiObject
對(duì)象后,您可以調(diào)用 UiObject
類中的方法,在由該對(duì)象表示的界面組件上執(zhí)行用戶交互。您可以指定如下操作:
-
click()
:點(diǎn)擊界面元素的可見(jiàn)邊界的中心。 -
dragTo()
:將此對(duì)象拖動(dòng)到任意坐標(biāo)。 -
setText()
:清除可修改字段的內(nèi)容后,設(shè)置該字段中的文本。相反,clearTextField()
方法用于清除可修改字段中的現(xiàn)有文本。 -
swipeUp()
:對(duì)UiObject
執(zhí)行向上滑動(dòng)操作。同樣,swipeDown()
、swipeLeft()
和swipeRight()
方法用于執(zhí)行相應(yīng)的操作。
通過(guò) UI Automator 測(cè)試框架,您可以發(fā)送 Intent
或啟動(dòng) Activity
,無(wú)需使用 shell 命令,只需通過(guò) getContext()
獲取 Context
對(duì)象即可。
以下代碼段展示了您的測(cè)試如何使用 Intent
啟動(dòng)被測(cè)應(yīng)用。當(dāng)您只想測(cè)試計(jì)算器應(yīng)用而不關(guān)心啟動(dòng)器時(shí),此方法很有用。
fun setUp() { ... // Launch a simple calculator app val context = getInstrumentation().context val intent = context.packageManager.getLaunchIntentForPackage(CALC_PACKAGE).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) } // Clear out any previous instances context.startActivity(intent) device.wait(Until.hasObject(By.pkg(CALC_PACKAGE).depth(0)), TIMEOUT) }
對(duì)集合執(zhí)行操作
如果需要模擬內(nèi)容集合(例如,音樂(lè)專輯中的歌曲或收件箱中的電子郵件列表)上的用戶交互,請(qǐng)使用 UiCollection
類。要?jiǎng)?chuàng)建 UiCollection
對(duì)象,請(qǐng)指定 UiSelector
,用于搜索其他子界面元素的界面容器或封裝容器,如包含子界面元素的布局視圖。
以下代碼段展示了您的測(cè)試如何構(gòu)建 UiCollection
以表示 FrameLayout
中顯示的視頻專輯:
val videos = UiCollection(UiSelector().className("android.widget.FrameLayout")) // Retrieve the number of videos in this collection: val count = videos.getChildCount( UiSelector().className("android.widget.LinearLayout") ) // Find a specific video and simulate a user-click on it val video: UiObject = videos.getChildByText( UiSelector().className("android.widget.LinearLayout"), "Cute Baby Laughing" ) video.click() // Simulate selecting a checkbox that is associated with the video val checkBox: UiObject = video.getChild( UiSelector().className("android.widget.Checkbox") ) if (!checkBox.isSelected) checkBox.click()
對(duì)可滾動(dòng)視圖執(zhí)行操作
使用 UiScrollable
類模擬顯示屏上的垂直或水平滾動(dòng)。當(dāng)界面元素位于屏幕外而您需要滾動(dòng)屏幕以使其進(jìn)入視野時(shí),此方法很有用。
以下代碼段展示了如何模擬向下滾動(dòng)“設(shè)置”菜單并點(diǎn)擊“關(guān)于平板電腦”選項(xiàng)的操作:
val settingsItem = UiScrollable(UiSelector().className("android.widget.ListView")) val about: UiObject = settingsItem.getChildByText( UiSelector().className("android.widget.LinearLayout"), "About tablet" ) about.click()
驗(yàn)證結(jié)果
InstrumentationTestCase
擴(kuò)展了 TestCase
,因此您可以使用標(biāo)準(zhǔn)的 JUnit Assert
方法測(cè)試應(yīng)用中的界面組件是否會(huì)返回預(yù)期結(jié)果。
以下代碼段展示了您的測(cè)試如何找到計(jì)算器應(yīng)用中的幾個(gè)按鈕,按順序點(diǎn)擊它們,然后驗(yàn)證是否顯示了正確的結(jié)果。
private const val CALC_PACKAGE = "com.myexample.calc" fun testTwoPlusThreeEqualsFive() { // Enter an equation: 2 + 3 = ? device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("two")).click() device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("plus")).click() device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("three")).click() device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("equals")).click() // Verify the result = 5 val result: UiObject2 = device.findObject(By.res(CALC_PACKAGE, "result")) assertEquals("5", result.text) }
四、測(cè)試應(yīng)用組件集成
如果您的應(yīng)用使用了用戶不直接與之互動(dòng)的組件(如服務(wù)或內(nèi)容提供器),您應(yīng)驗(yàn)證這些組件在應(yīng)用中的行為方式是否正確。
在開(kāi)發(fā)此類組件時(shí),您應(yīng)養(yǎng)成編寫(xiě)集成測(cè)試的習(xí)慣,以便當(dāng)您的應(yīng)用在設(shè)備或模擬器上運(yùn)行時(shí)驗(yàn)證組件的行為。
注意:Android 不為 BroadcastReceiver
提供單獨(dú)的測(cè)試用例類。如需驗(yàn)證 BroadcastReceiver
是否正確響應(yīng),您可以測(cè)試向其發(fā)送 Intent
對(duì)象的組件。或者,您可以通過(guò)調(diào)用 ApplicationProvider.getApplicationContext()
來(lái)創(chuàng)建 BroadcastReceiver
的實(shí)例,然后調(diào)用要測(cè)試的 BroadcastReceiver
方法(通常,這是 onReceive()
方法)。
測(cè)試服務(wù)
如果您要實(shí)現(xiàn)將本地 Service
作為應(yīng)用的組件,則應(yīng)對(duì) Service
進(jìn)行測(cè)試,以確保其不會(huì)出現(xiàn)意外行為。您可以創(chuàng)建插樁單元測(cè)試以驗(yàn)證 Service
的行為是否正確;例如,服務(wù)是否存儲(chǔ)和返回有效的數(shù)據(jù)值并正確執(zhí)行數(shù)據(jù)操作。
AndroidX Test 提供了一個(gè) API,用于在隔離的環(huán)境中測(cè)試 Service
對(duì)象。ServiceTestRule 類是一個(gè) JUnit 4 規(guī)則,可在單元測(cè)試方法運(yùn)行之前啟動(dòng)服務(wù),并在測(cè)試完成后關(guān)閉服務(wù)。通過(guò)使用此測(cè)試規(guī)則,您可確保始終在測(cè)試方法運(yùn)行之前建立與服務(wù)的連接。如需詳細(xì)了解 JUnit 4 規(guī)則,請(qǐng)參閱 JUnit 文檔。
注意:ServiceTestRule 類不支持測(cè)試 IntentService
對(duì)象。如果您需要測(cè)試 IntentService
對(duì)象,應(yīng)將邏輯封裝在一個(gè)單獨(dú)的類中,并創(chuàng)建相應(yīng)的單元測(cè)試。
創(chuàng)建服務(wù)的集成測(cè)試
您的集成測(cè)試應(yīng)編寫(xiě)為 JUnit 4 測(cè)試類。如需詳細(xì)了解如何創(chuàng)建 JUnit 4 測(cè)試類以及如何使用 JUnit 4 斷言方法,請(qǐng)參閱創(chuàng)建插樁單元測(cè)試類。
如需創(chuàng)建服務(wù)的集成測(cè)試,請(qǐng)?jiān)跍y(cè)試類定義的開(kāi)頭添加 @RunWith(AndroidJUnit4::class)
注釋。您還需要將 AndroidX Test 提供的 AndroidJUnitRunner
類指定為默認(rèn)測(cè)試運(yùn)行程序。運(yùn)行插樁單元測(cè)試中對(duì)此步驟進(jìn)行了更詳細(xì)的說(shuō)明。
接下來(lái),使用 @Rule
注釋在測(cè)試中創(chuàng)建一個(gè) ServiceTestRule 實(shí)例。
@get:Rule val serviceRule = ServiceTestRule()
以下示例展示了如何實(shí)現(xiàn)服務(wù)的集成測(cè)試。測(cè)試方法 testWithBoundService
將驗(yàn)證應(yīng)用是否成功綁定到本地服務(wù),以及服務(wù)接口是否正常運(yùn)行。
@Test @Throws(TimeoutException::class) fun testWithBoundService() { // Create the service Intent. val serviceIntent = Intent( ApplicationProvider.getApplicationContext<Context>(), LocalService::class.java ).apply { // Data can be passed to the service via the Intent. putExtra(SEED_KEY, 42L) } // Bind the service and grab a reference to the binder. val binder: IBinder = serviceRule.bindService(serviceIntent) // Get the reference to the service, or you can call // public methods on the binder directly. val service: LocalService = (binder as LocalService.LocalBinder).getService() // Verify that the service is working correctly. assertThat(service.getRandomInt(), `is`(any(Int::class.java))) }
測(cè)試內(nèi)容提供器
為內(nèi)容提供程序創(chuàng)建集成測(cè)試
在 Android 中,應(yīng)用將內(nèi)容提供程序視為提供數(shù)據(jù)表而其內(nèi)部構(gòu)件不可見(jiàn)的 Google Data API。內(nèi)容提供程序可能具有許多公開(kāi)常量,但通常公開(kāi)方法即使有也很少,而且沒(méi)有公開(kāi)變量。因此,您應(yīng)僅根據(jù)提供程序的公開(kāi)成員編寫(xiě)測(cè)試。這樣設(shè)計(jì)的內(nèi)容提供程序在其自身與其用戶之間提供了約定。
內(nèi)容提供程序可讓您訪問(wèn)實(shí)際用戶數(shù)據(jù),因此請(qǐng)務(wù)必確保在隔離的測(cè)試環(huán)境中測(cè)試內(nèi)容提供程序。此方法只允許您針對(duì)在測(cè)試用例中明確設(shè)置的數(shù)據(jù)依賴項(xiàng)運(yùn)行。這也意味著,您的測(cè)試不會(huì)修改實(shí)際用戶數(shù)據(jù)。例如,您應(yīng)避免編寫(xiě)由于先前測(cè)試遺留了數(shù)據(jù)而失敗的測(cè)試。同樣,您的測(cè)試應(yīng)避免在提供程序中添加或刪除實(shí)際聯(lián)系信息。
如需在隔離環(huán)境中測(cè)試您的內(nèi)容提供程序,請(qǐng)使用 ProviderTestCase2
類。此類允許您使用 Android 模擬對(duì)象類(如 IsolatedContext
和 MockContentResolver
)訪問(wèn)文件和數(shù)據(jù)庫(kù)信息,而不會(huì)影響實(shí)際用戶數(shù)據(jù)。
您的集成測(cè)試應(yīng)編寫(xiě)為 JUnit 4 測(cè)試類。要詳細(xì)了解如何創(chuàng)建 JUnit 4 測(cè)試類以及如何使用 JUnit 4 斷言,請(qǐng)參閱創(chuàng)建本地單元測(cè)試類。
如需為您的內(nèi)容提供程序創(chuàng)建集成測(cè)試,必須執(zhí)行以下步驟:
-
將測(cè)試類創(chuàng)建為
ProviderTestCase2
的子類。 -
在測(cè)試類定義的開(kāi)頭添加
@RunWith(AndroidJUnit4::class)
注釋。 -
將 AndroidX Test 提供的
AndroidJUnitRunner
類指定為默認(rèn)測(cè)試運(yùn)行程序。 -
通過(guò)
ApplicationProvider
類設(shè)置Context
對(duì)象。請(qǐng)參閱以下代碼段中的示例。
@Throws(Exception::class) override fun setUp() { super.setUp() context = ApplicationProvider.getApplicationContext<Context>() }
ProviderTestCase2 的工作原理
您可以使用 ProviderTestCase2
的子類測(cè)試提供程序。該基類擴(kuò)展了 AndroidTestCase
,因此它提供了 JUnit 測(cè)試框架以及用于測(cè)試應(yīng)用權(quán)限的 Android 專用方法。該類最重要的功能是其初始化,在初始化過(guò)程中可創(chuàng)建隔離的測(cè)試環(huán)境。
初始化是在 ProviderTestCase2
的構(gòu)造函數(shù)中完成的,而子類會(huì)在其自己的構(gòu)造函數(shù)中調(diào)用該過(guò)程。ProviderTestCase2
構(gòu)造函數(shù)會(huì)創(chuàng)建一個(gè) IsolatedContext
對(duì)象,該對(duì)象允許執(zhí)行文件和數(shù)據(jù)庫(kù)操作,但會(huì)對(duì)與 Android 系統(tǒng)的其他交互進(jìn)行打樁。文件和數(shù)據(jù)庫(kù)操作本身發(fā)生在設(shè)備或模擬器本地的一個(gè)帶有特殊前綴的目錄中。
然后,該構(gòu)造函數(shù)會(huì)創(chuàng)建一個(gè) MockContentResolver
用作測(cè)試的解析器。
最后,該構(gòu)造函數(shù)會(huì)創(chuàng)建一個(gè)被測(cè)提供程序的實(shí)例。這是一個(gè)普通的 ContentProvider
對(duì)象,但它會(huì)從 IsolatedContext
獲取所有環(huán)境信息,因此只能在隔離的測(cè)試環(huán)境中工作。在測(cè)試用例類中完成的所有測(cè)試都針對(duì)該隔離對(duì)象運(yùn)行。
為內(nèi)容提供程序運(yùn)行集成測(cè)試的方式與插樁單元測(cè)試相同。
要測(cè)試的內(nèi)容
以下是關(guān)于測(cè)試內(nèi)容提供程序的一些具體準(zhǔn)則。
-
使用解析器方法進(jìn)行測(cè)試:即使您可以在
ProviderTestCase2
中實(shí)例化提供程序?qū)ο?,也?yīng)始終通過(guò)適當(dāng)?shù)?URI 使用解析器對(duì)象進(jìn)行測(cè)試。這樣做可確保您在測(cè)試提供程序時(shí)執(zhí)行的交互與常規(guī)應(yīng)用將會(huì)使用的交互相同。 -
以約定的形式測(cè)試公開(kāi)提供程序:如果您打算將您的提供程序公開(kāi)并使其可供其他應(yīng)用使用,應(yīng)以約定的形式對(duì)其進(jìn)行測(cè)試。有關(guān)如何執(zhí)行此操作的一些示例如下所示:
-
使用提供程序公開(kāi)的常量進(jìn)行測(cè)試。例如,查找在提供程序的其中一個(gè)數(shù)據(jù)表中引用列名稱的常量。這些常量應(yīng)始終為提供程序公開(kāi)定義的常量。
-
測(cè)試提供程序提供的所有 URI。提供程序可能會(huì)提供多個(gè) URI,每個(gè) URI 引用數(shù)據(jù)的不同方面。
-
測(cè)試無(wú)效的 URI:?jiǎn)卧獪y(cè)試應(yīng)故意使用無(wú)效的 URI 調(diào)用提供程序,并查找錯(cuò)誤。良好的提供程序設(shè)計(jì)是針對(duì)無(wú)效的 URI 拋出
IllegalArgumentException
。
-
-
測(cè)試標(biāo)準(zhǔn)提供程序交互:大多數(shù)提供程序都會(huì)提供六種訪問(wèn)方法:
query()
、insert()
、delete()
、update()
、getType()
和onCreate()
。您的測(cè)試應(yīng)驗(yàn)證所有這些方法是否有效。內(nèi)容提供程序主題中對(duì)這些方法進(jìn)行了更詳細(xì)的說(shuō)明。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-617243.html -
測(cè)試業(yè)務(wù)邏輯:如果內(nèi)容提供程序?qū)崿F(xiàn)了業(yè)務(wù)邏輯,應(yīng)對(duì)其進(jìn)行測(cè)試。業(yè)務(wù)邏輯包括處理無(wú)效值、財(cái)務(wù)或算術(shù)計(jì)算、消除或合并重復(fù)項(xiàng)。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-617243.html
到了這里,關(guān)于Android Unit Test的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!