- Espresso 測試框架教程
- Espresso 測試 - 首頁
- 介紹
- 設定說明
- 在 Android Studio 中執行測試
- JUnit 概述
- 架構
- 檢視匹配器 (View Matchers)
- 自定義檢視匹配器 (Custom View Matchers)
- 檢視斷言 (View Assertions)
- 檢視操作 (View Actions)
- 測試 AdapterView
- 測試 WebView
- 測試非同步操作
- 測試 Intent
- 測試多個應用程式的 UI
- 測試錄製器 (Test Recorder)
- 測試 UI 效能
- 測試輔助功能
- Espresso 測試資源
- Espresso 測試 - 快速指南
- Espresso 測試 - 有用資源
- Espresso 測試 - 討論
Espresso 測試框架 - 快速指南
Espresso 測試框架 - 介紹
總的來說,移動自動化測試是一項困難且具有挑戰性的任務。Android 在不同裝置和平臺上的可用性使得移動自動化測試變得繁瑣。為了簡化這一過程,Google 接受了這一挑戰並開發了 Espresso 框架。它提供了一個非常簡單、一致且靈活的 API 來自動化和測試 Android 應用程式中的使用者介面。Espresso 測試可以用 Java 和 Kotlin(一種用於開發 Android 應用程式的現代程式語言)編寫。
Espresso API 簡單易學。您可以輕鬆地執行 Android UI 測試,而無需多執行緒測試的複雜性。Google Drive、地圖和其他一些應用程式目前正在使用 Espresso。
Espresso 的特性
Espresso 支援的一些主要特性如下:
非常簡單的 API,易於學習。
高度可擴充套件和靈活。
提供單獨的模組來測試 Android WebView 元件。
提供單獨的模組來驗證和模擬 Android Intent。
提供應用程式和測試之間的自動同步。
Espresso 的優勢
讓我們來看看 Espresso 的好處。
向後相容性
易於設定。
高度穩定的測試周期。
也支援測試應用程式外部的活動。
支援 JUnit4
適用於編寫黑盒測試的 UI 自動化。
Espresso 測試框架 - 設定說明
在本章中,讓我們瞭解如何安裝 Espresso 框架,將其配置為編寫 Espresso 測試並在我們的 Android 應用程式中執行它。
先決條件
Espresso 是一個使用者介面測試框架,用於測試使用 Android SDK 以 Java/Kotlin 語言開發的 Android 應用程式。因此,Espresso 的唯一要求是使用 Android SDK 以 Java 或 Kotlin 開發應用程式,建議使用最新的 Android Studio。
在我們開始使用 Espresso 框架之前,需要正確配置以下各項:
安裝最新的 Java JDK 並配置 JAVA_HOME 環境變數。
安裝最新的 Android Studio(3.2 或更高版本)。
使用 SDK Manager 安裝最新的 Android SDK 並配置 ANDROID_HOME 環境變數。
安裝最新的 Gradle Build Tool 並配置 GRADLE_HOME 環境變數。
配置 Espresso 測試框架
最初,Espresso 測試框架作為 Android Support 庫的一部分提供。後來,Android 團隊提供了一個新的 Android 庫 AndroidX,並將最新的 Espresso 測試框架開發遷移到該庫。最新的 Espresso 測試框架開發(Android 9.0,API 級別 28 或更高)將在 AndroidX 庫中進行。
在專案中包含 Espresso 測試框架就像在應用程式 gradle 檔案 app/build.gradle 中將 Espresso 測試框架設定為依賴項一樣簡單,完整的配置如下:
使用 Android Support 庫:
android {
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espressocore:3.0.2'
}
使用 AndroidX 庫:
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.androidx.test:runner:1.0.2'
androidTestImplementation 'com.androidx.espresso:espresso-core:3.0.2'
}
android/defaultConfig 中的 testInstrumentationRunner 設定 AndroidJUnitRunner 類來執行 Instrumentation 測試。dependencies 中的第一行包含 JUnit 測試框架,第二行包含執行測試用例的測試執行器庫,最後第三行包含 Espresso 測試框架。
預設情況下,Android Studio 在建立 Android 專案時將 Espresso 測試框架(Android Support 庫)設定為依賴項,Gradle 將從 Maven 儲存庫下載必要的庫。讓我們建立一個簡單的 Hello world Android 應用程式,並檢查 Espresso 測試框架是否已正確配置。
建立新 Android 應用程式的步驟如下:
啟動 Android Studio。
選擇 File → New → New Project。
輸入應用程式名稱 (HelloWorldApp) 和公司域名 (espressosamples.tutorialspoint.com),然後單擊 Next。
建立 Android 專案:
選擇最小 API 為 API 15:Android 4.0.3 (IceCreamSandwich),然後單擊 Next。
選擇目標 Android 裝置:
選擇 Empty Activity,然後單擊 Next。
向移動裝置新增活動:
輸入主活動的名稱,然後單擊 Finish。
配置活動:
建立新專案後,開啟 app/build.gradle 檔案並檢查其內容。該檔案的內容如下所示:
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.tutorialspoint.espressosamples.helloworldapp"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espressocore:3.0.2'
}
最後一行指定了 Espresso 測試框架依賴項。預設情況下,配置的是 Android Support 庫。我們可以透過單擊選單中的 Refactor → Migrate to AndroidX 來重新配置應用程式以使用 AndroidX 庫。
遷移到 AndroidX:
現在,app/build.gradle 的更改如下所示:
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.tutorialspoint.espressosamples.helloworldapp"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
現在,最後一行包含來自 AndroidX 庫的 Espresso 測試框架。
裝置設定
在測試期間,建議關閉 Android 裝置上用於測試的動畫。這將減少檢查空閒資源時的混淆。
讓我們看看如何在 Android 裝置上停用動畫 – (設定 → 開發者選項):
視窗動畫縮放比例
過渡動畫縮放比例
動畫持續時間縮放比例
如果“開發者選項”選單在“設定”螢幕中不可用,則多次單擊“關於手機”選項中的“版本號”。這將啟用“開發者選項”選單。
在 Android Studio 中執行測試
在本章中,讓我們看看如何使用 Android Studio 執行測試。
每個 Android 應用程式都有兩種型別的測試:
功能/單元測試
Instrumentation 測試
功能測試不需要在裝置或模擬器中安裝和啟動實際的 Android 應用程式,並測試其功能。它可以在控制檯中本身啟動,而無需呼叫實際的應用程式。但是,Instrumentation 測試需要啟動實際應用程式才能測試功能,例如使用者介面和使用者互動。預設情況下,單元測試寫在 **src/test/java/** 資料夾中,Instrumentation 測試寫在 **src/androidTest/java/** 資料夾中。Android Studio 為測試類提供 Run 上下文選單,以執行在所選測試類中編寫的測試。預設情況下,Android 應用程式有兩個類:src/test 資料夾中的 ExampleUnitTest 和 src/androidTest 資料夾中的 ExampleInstrumentedTest。
要執行預設單元測試,請在 Android Studio 中選擇 ExampleUnitTest,右鍵單擊它,然後單擊“執行 'ExampleUnitTest'”,如下所示:
執行單元測試
這將執行單元測試並在控制檯中顯示結果,如下面的螢幕截圖所示:
單元測試成功
要執行預設 Instrumentation 測試,請在 Android Studio 中選擇 ExampleInstrumentationTest,右鍵單擊它,然後單擊“執行 'ExampleInstrumentationTest'”,如下所示:
執行 Instrumentation 測試
這將透過在裝置或模擬器中啟動應用程式來執行單元測試,並在控制檯中顯示結果,如下面的螢幕截圖所示:
Instrumentation 測試執行成功。
Espresso 測試框架 - JUnit 概述
在本章中,讓我們瞭解 JUnit 的基礎知識,JUnit 是 Java 社群開發的流行單元測試框架,Espresso 測試框架就是基於它構建的。
JUnit 是 Java 應用程式單元測試的事實標準。儘管它流行於單元測試,但它也完全支援 Instrumentation 測試。Espresso 測試庫擴充套件了必要的 JUnit 類以支援基於 Android 的 Instrumentation 測試。
編寫簡單的單元測試
讓我們建立一個 Java 類 Computation(Computation.java),並編寫簡單的數學運算 Summation 和 Multiplication。然後,我們將使用 JUnit 編寫測試用例,並透過執行測試用例來檢查它。
啟動 Android Studio。
開啟上一章中建立的 HelloWorldApp。
在 app/src/main/java/com/tutorialspoint/espressosamples/helloworldapp/ 中建立一個檔案 Computation.java,並編寫兩個函式 – Sum 和 Multiply,如下所示:
package com.tutorialspoint.espressosamples.helloworldapp;
public class Computation {
public Computation() {}
public int Sum(int a, int b) {
return a + b;
}
public int Multiply(int a, int b) {
return a * b;
}
}
在 app/src/test/java/com/tutorialspoint/espressosamples/helloworldapp 中建立一個檔案 ComputationUnitTest.java,並編寫單元測試用例來測試 Sum 和 Multiply 功能,如下所示:
package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ComputationUnitTest {
@Test
public void sum_isCorrect() {
Computation computation = new Computation();
assertEquals(4, computation.Sum(2,2));
}
@Test
public void multiply_isCorrect() {
Computation computation = new Computation();
assertEquals(4, computation.Multiply(2,2));
}
}
在這裡,我們使用了兩個新術語 – @Test 和 assertEquals。一般來說,JUnit 使用 Java 註解來識別類中的測試用例以及如何執行測試用例的資訊。@Test 是一個這樣的 Java 註解,它指定特定函式是 JUnit 測試用例。assertEquals 是一個函式,用於斷言第一個引數(預期值)和第二個引數(計算值)相等且相同。JUnit 為不同的測試場景提供許多斷言方法。
現在,透過右鍵單擊該類並呼叫“執行 'ComputationUnitTest'”選項(如上一章所述)來在 Android Studio 中執行 ComputationUnitTest。這將執行單元測試用例並報告成功。
計算單元測試的結果如下所示:
註解
JUnit 框架廣泛使用註解。一些重要的註解如下:
@Test
@Before
@After
@BeforeClass
@AfterClass
@Rule
@Test 註解
@Test 註解是 JUnit 框架中非常重要的註解。@Test 用於區分普通方法和測試用例方法。一旦用 @Test 註解修飾一個方法,那麼該方法就被認為是一個 測試用例,並將由 JUnit Runner 執行。JUnit Runner 是一個特殊的類,用於查詢和執行 Java 類中可用的 JUnit 測試用例。目前,我們使用 Android Studio 的內建選項來執行單元測試(進而執行 JUnit Runner)。示例程式碼如下:
package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ComputationUnitTest {
@Test
public void multiply_isCorrect() {
Computation computation = new Computation();
assertEquals(4, computation.Multiply(2,2));
}
}
@Before
@Before 註解用於引用一個方法,該方法需要在執行特定測試類中任何測試方法之前呼叫。例如,在我們的示例中,可以在單獨的方法中建立 Computation 物件並用 @Before 註解,以便它在 sum_isCorrect 和 multiply_isCorrect 測試用例之前執行。完整的程式碼如下:
package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ComputationUnitTest {
Computation computation = null;
@Before
public void CreateComputationObject() {
this.computation = new Computation();
}
@Test
public void sum_isCorrect() {
assertEquals(4, this.computation.Sum(2,2));
}
@Test
public void multiply_isCorrect() {
assertEquals(4, this.computation.Multiply(2,2));
}
}
@After
@After 與 @Before 類似,但是用 @After 註解的方法將在每個測試用例執行後被呼叫或執行。示例程式碼如下:
package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ComputationUnitTest {
Computation computation = null;
@Before
public void CreateComputationObject() {
this.computation = new Computation();
}
@After
public void DestroyComputationObject() {
this.computation = null;
}
@Test
public void sum_isCorrect() {
assertEquals(4, this.computation.Sum(2,2));
}
@Test
public void multiply_isCorrect() {
assertEquals(4, this.computation.Multiply(2,2));
}
}
@BeforeClass
@BeforeClass 與 @Before 類似,但是用 @BeforeClass 註解的方法只會在特定類中執行所有測試用例之前呼叫或執行一次。它對於建立資料庫連線物件等資源密集型物件很有用。這將減少執行一組測試用例的時間。此方法需要是靜態的才能正常工作。在我們的示例中,我們可以在執行所有測試用例之前建立一次計算物件,如下所示:
package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ComputationUnitTest {
private static Computation computation = null;
@BeforeClass
public static void CreateComputationObject() {
computation = new Computation();
}
@Test
public void sum_isCorrect() {
assertEquals(4, computation.Sum(2,2));
}
@Test
public void multiply_isCorrect() {
assertEquals(4, computation.Multiply(2,2));
}
}
@AfterClass
@AfterClass 與 @BeforeClass 類似,但是用 @AfterClass 註解的方法只會在特定類中所有測試用例執行後呼叫或執行一次。此方法也需要是靜態的才能正常工作。示例程式碼如下:
package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ComputationUnitTest {
private static Computation computation = null;
@BeforeClass
public static void CreateComputationObject() {
computation = new Computation();
}
@AfterClass
public static void DestroyComputationObject() {
computation = null;
}
@Test
public void sum_isCorrect() {
assertEquals(4, computation.Sum(2,2));
}
@Test
public void multiply_isCorrect() {
assertEquals(4, computation.Multiply(2,2));
}
}
@Rule
@Rule 註解是 JUnit 的亮點之一。它用於向測試用例新增行為。我們只能註解 TestRule 型別的欄位。它實際上提供了 @Before 和 @After 註解提供的功能集,但以更有效和可重用的方式。例如,我們可能需要一個臨時資料夾來在測試用例期間儲存一些資料。通常,我們需要在執行測試用例之前建立一個臨時資料夾(使用 @Before 或 @BeforeClass 註解),並在測試用例執行後將其銷燬(使用 @After 或 @AfterClass 註解)。相反,我們可以使用 JUnit 框架提供的 TemporaryFolder(TestRule 型別)類為所有測試用例建立一個臨時資料夾,並且臨時資料夾將在測試用例執行時被刪除。我們需要建立一個新的 TemporaryFolder 型別變數,並用 @Rule 註解,如下所示:
package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
public class ComputationUnitTest {
private static Computation computation = null;
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void file_isCreated() throws IOException {
folder.newFolder("MyTestFolder");
File testFile = folder.newFile("MyTestFile.txt");
assertTrue(testFile.exists());
}
@BeforeClass
public static void CreateComputationObject() {
computation = new Computation();
}
@AfterClass
public static void DestroyComputationObject() {
computation = null;
}
@Test
public void sum_isCorrect() {
assertEquals(4, computation.Sum(2,2));
}
@Test
public void multiply_isCorrect() {
assertEquals(4, computation.Multiply(2,2));
}
}
執行順序
在 JUnit 中,用不同註解註釋的方法將按如下所示的特定順序執行:
@BeforeClass
@Rule
@Before
@Test
@After
@AfterClass
斷言
斷言是一種檢查測試用例的預期值是否與測試用例結果的實際值匹配的方法。JUnit 為不同的場景提供了斷言;下面列出了一些重要的斷言:
fail() − 顯式地使測試用例失敗。
assertTrue(boolean test_condition) − 檢查 test_condition 是否為真
assertFalse(boolean test_condition) − 檢查 test_condition 是否為假
assertEquals(expected, actual) − 檢查兩個值是否相等
assertNull(object) − 檢查物件是否為空
assertNotNull(object) − 檢查物件是否不為空
assertSame(expected, actual) − 檢查兩者是否引用同一個物件。
assertNotSame(expected, actual) − 檢查兩者是否引用不同的物件。
Espresso 測試框架 - 架構
在本章中,讓我們學習 Espresso 測試框架的術語,如何編寫簡單的 Espresso 測試用例以及 Espresso 測試框架的完整工作流程或架構。
概述
Espresso 提供大量類來測試 Android 應用程式的使用者介面和使用者互動。它們可以分為以下五類:
JUnit 執行器
Android 測試框架提供了一個執行器 AndroidJUnitRunner 來執行以 JUnit3 和 JUnit4 風格編寫的 Espresso 測試用例。它特定於 Android 應用程式,它透明地處理在實際裝置或模擬器中載入 Espresso 測試用例和被測應用程式,執行測試用例並報告測試用例的結果。要在測試用例中使用 AndroidJUnitRunner,我們需要使用 @RunWith 註解來註解測試類,然後傳遞 AndroidJUnitRunner 引數,如下所示:
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
}
JUnit 規則
Android 測試框架提供了一個規則 ActivityTestRule,用於在執行測試用例之前啟動 Android 活動。它在每個用 @Test 和 @Before 註解的方法之前啟動活動。它將在用 @After 註解的方法之後終止活動。示例程式碼如下:
@Rule public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);
這裡,MainActivity 是在執行測試用例之前啟動並在特定測試用例執行後銷燬的活動。
ViewMatchers
Espresso 提供大量 ViewMatcher 類(在 androidx.test.espresso.matcher.ViewMatchers 包中)來匹配和查詢 Android 活動螢幕檢視層次結構中的 UI 元素/檢視。Espresso 的 onView 方法採用單個 Matcher(檢視匹配器)型別的引數,查詢相應的 UI 檢視並返回相應的 ViewInteraction 物件。onView 方法返回的 ViewInteraction 物件可以進一步用於呼叫諸如點選匹配檢視之類的操作,或者可以用於斷言匹配檢視。查詢文字為“Hello World!”的檢視的示例程式碼如下:
ViewInteraction viewInteraction = Espresso.onView(withText("Hello World!"));
這裡,withText 是一個匹配器,可用於匹配文字為“Hello World!”的 UI 檢視。
ViewActions
Espresso 提供大量 ViewAction 類(在 androidx.test.espresso.action.ViewActions 中)來對所選/匹配的檢視呼叫不同的操作。一旦 onView 匹配並返回 ViewInteraction 物件,就可以透過呼叫 ViewInteraction 物件的“perform”方法並使用適當的檢視操作傳遞它來呼叫任何操作。點選匹配檢視的示例程式碼如下:
ViewInteraction viewInteraction = Espresso.onView(withText("Hello World!"));
viewInteraction.perform(click());
這裡,將呼叫匹配檢視的點選操作。
ViewAssertions
與檢視匹配器和檢視操作類似,Espresso 提供大量檢視斷言(在 androidx.test.espresso.assertion.ViewAssertions 包中)來斷言匹配的檢視是我們期望的。一旦 onView 匹配並返回 ViewInteraction 物件,就可以透過使用適當的檢視斷言將其傳遞給 ViewInteraction 的 check 方法來檢查任何斷言。斷言匹配檢視的示例程式碼如下:
ViewInteraction viewInteraction = Espresso.onView(withText("Hello World!"));
viewInteraction.check(matches(withId(R.id.text_view)));
這裡,matches 接受檢視匹配器並返回檢視斷言,可以透過 ViewInteraction 的 check 方法進行檢查。
Espresso 測試框架的工作流程
讓我們瞭解 Espresso 測試框架是如何工作的,以及它如何提供簡單靈活的方式來進行任何型別的使用者互動。Espresso 測試用例的工作流程如下所述:
正如我們前面學到的,Android JUnit 執行器 AndroidJUnit4 將執行 Android 測試用例。Espresso 測試用例需要用 @RunWith(AndroidJUnut.class) 標記。首先,AndroidJUnit4 將準備執行測試用例的環境。它啟動連線的 Android 裝置或模擬器,安裝應用程式並確保被測應用程式處於就緒狀態。它將執行測試用例並報告結果。
Espresso 至少需要一個 ActivityTestRule 型別的 JUnit 規則來指定活動。Android JUnit 執行器將使用 ActivityTestRule 啟動要啟動的活動。
每個測試用例至少需要呼叫一次 onView 或 onDate(用於查詢基於資料的檢視,例如 AdapterView)方法來匹配和查詢所需的檢視。onView 或 onData 返回 ViewInteraction 物件。
一旦返回 ViewInteraction 物件,我們就可以呼叫所選檢視的操作,或者使用斷言檢查檢視的預期檢視。
可以使用 ViewInteraction 物件的 perform 方法透過傳遞任何一個可用的檢視操作來呼叫操作。
可以使用 ViewInteraction 物件的 check 方法透過傳遞任何一個可用的檢視斷言來呼叫斷言。
工作流程 的圖表表示如下:
示例 - 檢視斷言
讓我們編寫一個簡單的測試用例來查詢我們的“HelloWorldApp”應用程式中具有“Hello World!”文字的文字檢視,然後使用檢視斷言對其進行斷言。完整的程式碼如下:
package com.tutorialspoint.espressosamples.helloworldapp;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.ViewMatchers.withText;;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Rule
public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void view_isCorrect() {
onView(withText("Hello World!")).check(matches(isDisplayed()));
}
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.tutorialspoint.espressosamples.helloworldapp", appContext.getPackageName());
}
}
這裡,我們使用了 withText 檢視匹配器來查詢具有“Hello World!”文字的文字檢視,並使用 matches 檢視斷言來斷言文字檢視是否正確顯示。一旦在 Android Studio 中呼叫測試用例,它將執行測試用例並報告如下成功訊息。
view_isCorrect 測試用例
Espresso 測試框架 - 檢視匹配器
Espresso 框架提供許多檢視匹配器。匹配器的目的是使用檢視的不同屬性(如 Id、文字和子檢視的可用性)來匹配檢視。每個匹配器都匹配檢視的特定屬性並應用於特定型別的檢視。例如,withId 匹配器匹配檢視的 Id 屬性並應用於所有檢視,而 withText 匹配器匹配檢視的 Text 屬性並僅應用於 TextView。
在本章中,讓我們學習 Espresso 測試框架提供的不同匹配器,以及學習 Espresso 匹配器構建的基礎 Hamcrest 庫。
Hamcrest 庫
Hamcrest 庫是 Espresso 測試框架範圍內的重要庫。Hamcrest 本身是一個用於編寫匹配器物件的框架。Espresso 框架廣泛使用 Hamcrest 庫,並在必要時對其進行擴充套件以提供簡單且可擴充套件的匹配器。
Hamcrest 提供了一個簡單的函式 assertThat 和一系列匹配器來斷言任何物件。assertThat 有三個引數,如下所示:
字串(測試描述,可選)
物件(實際值)
匹配器(預期值)
讓我們編寫一個簡單的示例來測試列表物件是否具有預期值。
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.MatcherAssert.assertThat;
@Test
public void list_hasValue() {
ArrayList<String> list = new ArrayList<String>();
list.add("John");
assertThat("Is list has John?", list, hasItem("John"));
}
這裡,hasItem 返回一個匹配器,它檢查實際列表是否將指定值作為其中一項。
Hamcrest 具有許多內建匹配器,以及建立新匹配器的選項。一些在 Espresso 測試框架中很有用的重要內建匹配器如下:
anything - 總是匹配
基於邏輯的匹配器
allOf − 接受任意數量的匹配器,並且只有在所有匹配器都成功時才匹配。
anyOf − 接受任意數量的匹配器,如果任何一個匹配器成功則匹配。
not − 接受一個匹配器,並且只有在匹配器失敗時才匹配,反之亦然。
基於文字的匹配器
equalToIgnoringCase − 用於測試實際輸入是否等於忽略大小寫的預期字串。
equalToIgnoringWhiteSpace − 用於測試實際輸入是否等於忽略大小寫和空格的指定字串。
containsString − 用於測試實際輸入是否包含指定的字串。
endsWith − 用於測試實際輸入是否以指定的字串開頭。(原文錯誤,應為結尾)
startsWith − 用於測試實際輸入是否以指定的字串結尾。(原文錯誤,應為開頭)
基於數字的匹配器
closeTo − 用於測試實際輸入是否接近預期數字。
greaterThan − 用於測試實際輸入是否大於預期數字。
greaterThanOrEqualTo − 用於測試實際輸入是否大於或等於預期數字。
lessThan − 用於測試實際輸入是否小於預期數字。
lessThanOrEqualTo − 用於測試實際輸入是否小於或等於預期數字。
基於物件的匹配器
equalTo − 用於測試實際輸入是否等於預期物件。
hasToString − 用於測試實際輸入是否有toString方法。
instanceOf − 用於測試實際輸入是否是預期類的例項。
isCompatibleType − 用於測試實際輸入是否與預期型別相容。
notNullValue − 用於測試實際輸入是否不為空。
sameInstance − 用於測試實際輸入和預期值是否是同一個例項。
hasProperty − 用於測試實際輸入是否具有預期的屬性。
is − `equalTo` 的簡寫或語法糖
匹配器
Espresso 提供 onView() 方法來匹配和查詢檢視。它接收檢視匹配器並返回 ViewInteraction 物件來與匹配的檢視進行互動。常用檢視匹配器列表如下:
withId()
withId() 接受一個 int 型別引數,該引數指的是檢視的 ID。它返回一個匹配器,該匹配器使用檢視的 ID 來匹配檢視。示例程式碼如下:
onView(withId(R.id.testView))
withText()
withText() 接受一個字串型別引數,該引數指的是檢視文字屬性的值。它返回一個匹配器,該匹配器使用檢視的文字值來匹配檢視。僅適用於 TextView。示例程式碼如下:
onView(withText("Hello World!"))
withContentDescription()
withContentDescription() 接受一個字串型別引數,該引數指的是檢視內容描述屬性的值。它返回一個匹配器,該匹配器使用檢視的描述來匹配檢視。示例程式碼如下:
onView(withContentDescription("blah"))
我們也可以傳遞文字值的資源 ID,而不是文字本身。
onView(withContentDescription(R.id.res_id_blah))
hasContentDescription()
hasContentDescription() 沒有引數。它返回一個匹配器,該匹配器匹配具有任何內容描述的檢視。示例程式碼如下:
onView(allOf(withId(R.id.my_view_id), hasContentDescription()))
withTagKey()
withTagKey() 接受一個字串型別引數,該引數指的是檢視的標籤鍵。它返回一個匹配器,該匹配器使用其標籤鍵來匹配檢視。示例程式碼如下:
onView(withTagKey("blah"))
我們也可以傳遞標籤名稱的資源 ID,而不是標籤名稱本身。
onView(withTagKey(R.id.res_id_blah))
withTagValue()
withTagValue() 接受一個 Matcher<Object> 型別引數,該引數指的是檢視的標籤值。它返回一個匹配器,該匹配器使用其標籤值來匹配檢視。示例程式碼如下:
onView(withTagValue(is((Object) "blah")))
這裡,is 是 Hamcrest 匹配器。
withClassName()
withClassName() 接受一個 Matcher<String> 型別引數,該引數指的是檢視的類名值。它返回一個匹配器,該匹配器使用其類名來匹配檢視。示例程式碼如下:
onView(withClassName(endsWith("EditText")))
這裡,endsWith 是 Hamcrest 匹配器,並返回 Matcher<String>。
withHint()
withHint() 接受一個 Matcher<String> 型別引數,該引數指的是檢視的提示值。它返回一個匹配器,該匹配器使用檢視的提示來匹配檢視。示例程式碼如下:
onView(withClassName(endsWith("Enter name")))
withInputType()
withInputType() 接受一個 int 型別引數,該引數指的是檢視的輸入型別。它返回一個匹配器,該匹配器使用其輸入型別來匹配檢視。示例程式碼如下:
onView(withInputType(TYPE_CLASS_DATETIME))
這裡,TYPE_CLASS_DATETIME 指的是支援日期和時間的編輯檢視。
withResourceName()
withResourceName() 接受一個 Matcher<String> 型別引數,該引數指的是檢視的類名值。它返回一個匹配器,該匹配器使用檢視的資源名稱來匹配檢視。示例程式碼如下:
onView(withResourceName(endsWith("res_name")))
它也接受字串引數。示例程式碼如下:
onView(withResourceName("my_res_name"))
withAlpha()
withAlpha() 接受一個 float 型別引數,該引數指的是檢視的 alpha 值。它返回一個匹配器,該匹配器使用檢視的 alpha 值來匹配檢視。示例程式碼如下:
onView(withAlpha(0.8))
withEffectiveVisibility()
withEffectiveVisibility() 接受一個 ViewMatchers.Visibility 型別引數,該引數指的是檢視的有效可見性。它返回一個匹配器,該匹配器使用檢視的可見性來匹配檢視。示例程式碼如下:
onView(withEffectiveVisibility(withEffectiveVisibility.INVISIBLE))
withSpinnerText()
withSpinnerText() 接受一個 Matcher<String> 型別引數,該引數指的是 Spinner 當前選定檢視的值。它返回一個匹配器,該匹配器根據其選定專案的 toString 值來匹配 spinner。示例程式碼如下:
onView(withSpinnerText(endsWith("USA")))
它也接受字串引數或字串的資源 ID。示例程式碼如下:
onView(withResourceName("USA"))
onView(withResourceName(R.string.res_usa))
withSubstring()
withSubString() 與 withText() 類似,不同之處在於它有助於測試檢視文字值的子字串。
onView(withSubString("Hello"))
hasLinks()
hasLinks() 沒有引數,它返回一個匹配器,該匹配器匹配具有連結的檢視。僅適用於 TextView。示例程式碼如下:
onView(allOf(withSubString("Hello"), hasLinks()))
這裡,allOf 是一個 Hamcrest 匹配器。allOf 返回一個匹配器,該匹配器匹配所有傳入的匹配器,在這裡,它用於匹配檢視以及檢查檢視的文字值中是否包含連結。
hasTextColor()
hasTextColor() 接受一個 int 型別引數,該引數指的是顏色的資源 ID。它返回一個匹配器,該匹配器根據其顏色來匹配 TextView。僅適用於 TextView。示例程式碼如下:
onView(allOf(withSubString("Hello"), hasTextColor(R.color.Red)))
hasEllipsizedText()
hasEllipsizedText() 沒有引數。它返回一個匹配器,該匹配器匹配具有長文字且已省略號顯示(first.. ten.. last)或被截斷(first…)的 TextView。示例程式碼如下:
onView(allOf(withId(R.id.my_text_view_id), hasEllipsizedText()))
hasMultilineText()
hasMultilineText() 沒有引數。它返回一個匹配器,該匹配器匹配具有任何多行文字的 TextView。示例程式碼如下:
onView(allOf(withId(R.id.my_test_view_id), hasMultilineText()))
hasBackground()
hasBackground() 接受一個 int 型別引數,該引數指的是背景資源的資源 ID。它返回一個匹配器,該匹配器根據其背景資源來匹配檢視。示例程式碼如下:
onView(allOf(withId("image"), hasBackground(R.drawable.your_drawable)))
hasErrorText()
hasErrorText() 接受一個 Matcher<String> 型別引數,該引數指的是檢視(EditText)的錯誤字串值。它返回一個匹配器,該匹配器使用檢視的錯誤字串來匹配檢視。僅適用於 EditText。示例程式碼如下:
onView(allOf(withId(R.id.editText_name), hasErrorText(is("name is required"))))
它也接受字串引數。示例程式碼如下:
onView(allOf(withId(R.id.editText_name), hasErrorText("name is required")))
hasImeAction()
hasImeAction() 接受一個 Matcher<Integer> 型別引數,該引數指的是檢視(EditText)支援的輸入方法。它返回一個匹配器,該匹配器使用檢視支援的輸入方法來匹配檢視。僅適用於 EditText。示例程式碼如下:
onView(allOf(withId(R.id.editText_name), hasImeAction(is(EditorInfo.IME_ACTION_GO))))
這裡,EditorInfo.IME_ACTION_GO 是輸入方法選項之一。hasImeAction() 也接受整數引數。示例程式碼如下:
onView(allOf(withId(R.id.editText_name), hasImeAction(EditorInfo.IME_ACTION_GO)))
supportsInputMethods()
supportsInputMethods() 沒有引數。如果檢視支援輸入方法,則它返回一個匹配器,該匹配器匹配該檢視。示例程式碼如下:
onView(allOf(withId(R.id.editText_name), supportsInputMethods()))
isRoot()
isRoot() 沒有引數。它返回一個匹配器,該匹配器匹配根檢視。示例程式碼如下:
onView(allOf(withId(R.id.my_root_id), isRoot()))
isDisplayed()
isDisplayed() 沒有引數。它返回一個匹配器,該匹配器匹配當前顯示的檢視。示例程式碼如下:
onView(allOf(withId(R.id.my_view_id), isDisplayed()))
isDisplayingAtLeast()
isDisplayingAtLeast() 接受一個 int 型別引數。它返回一個匹配器,該匹配器匹配當前至少顯示指定百分比的檢視。示例程式碼如下:
onView(allOf(withId(R.id.my_view_id), isDisplayingAtLeast(75)))
isCompletelyDisplayed()
isCompletelyDisplayed() 沒有引數。它返回一個匹配器,該匹配器匹配當前完全顯示在螢幕上的檢視。示例程式碼如下:
onView(allOf(withId(R.id.my_view_id), isCompletelyDisplayed()))
isEnabled()
isEnabled() 沒有引數。它返回一個匹配器,該匹配器匹配已啟用的檢視。示例程式碼如下:
onView(allOf(withId(R.id.my_view_id), isEnabled()))
isFocusable()
isFocusable() 沒有引數。它返回一個匹配器,該匹配器匹配具有焦點選項的檢視。示例程式碼如下:
onView(allOf(withId(R.id.my_view_id), isFocusable()))
hasFocus()
hasFocus() 沒有引數。它返回一個匹配器,該匹配器匹配當前獲得焦點的檢視。示例程式碼如下:
onView(allOf(withId(R.id.my_view_id), hasFocus()))
isClickable()
isClickable() 沒有引數。它返回一個匹配器,該匹配器匹配具有單擊選項的檢視。示例程式碼如下:
onView(allOf(withId(R.id.my_view_id), isClickable()))
isSelected()
isSelected() 沒有引數。它返回一個匹配器,該匹配器匹配當前選定的檢視。示例程式碼如下:
onView(allOf(withId(R.id.my_view_id), isSelected()))
isChecked()
isChecked() 沒有引數。它返回一個匹配器,該匹配器匹配型別為 CompoundButton(或其子型別)且處於選中狀態的檢視。示例程式碼如下:
onView(allOf(withId(R.id.my_view_id), isChecked()))
isNotChecked()
isNotChecked() 正好與 isChecked 相反。示例程式碼如下:
onView(allOf(withId(R.id.my_view_id), isNotChecked()))
isJavascriptEnabled()
isJavascriptEnabled() 沒有引數。它返回一個匹配器,該匹配器匹配正在評估 JavaScript 的 WebView。示例程式碼如下:
onView(allOf(withId(R.id.my_webview_id), isJavascriptEnabled()))
withParent()
withParent() 接受一個 Matcher<View> 型別引數。該引數指的是一個檢視。它返回一個匹配器,該匹配器匹配指定檢視作為父檢視的檢視。示例程式碼如下:
onView(allOf(withId(R.id.childView), withParent(withId(R.id.parentView))))
hasSibling()
hasSibling() 接受一個 Matcher<View> 型別引數。該引數指的是一個檢視。它返回一個匹配器,該匹配器匹配傳入檢視是其同級檢視之一的檢視。示例程式碼如下:
onView(hasSibling(withId(R.id.siblingView)))
withChild()
withChild() 接受一個 Matcher<View> 型別引數。該引數指的是一個檢視。它返回一個匹配器,該匹配器匹配傳入檢視是子檢視的檢視。示例程式碼如下:
onView(allOf(withId(R.id.parentView), withChild(withId(R.id.childView))))
hasChildCount()
hasChildCount() 接受一個 int 型別引數。該引數指的是檢視的子檢視數量。它返回一個匹配器,該匹配器匹配子檢視數量與引數中指定的數量完全相同的檢視。示例程式碼如下:
onView(hasChildCount(4))
hasMinimumChildCount()
hasMinimumChildCount() 接受一個 int 型別引數。該引數指的是檢視的子檢視數量。它返回一個匹配器,該匹配器匹配子檢視數量至少與引數中指定的數量相同的檢視。示例程式碼如下:
onView(hasMinimumChildCount(4))
hasDescendant()
hasDescendant() 接受一個 Matcher<View> 型別引數。該引數指的是一個檢視。它返回一個匹配器,該匹配器匹配傳入檢視是檢視層次結構中後代檢視之一的檢視。示例程式碼如下:
onView(hasDescendant(withId(R.id.descendantView)))
isDescendantOfA()
isDescendantOfA() 接受一個 Matcher<View> 型別的引數。該引數指向一個檢視。它返回一個匹配器,該匹配器匹配傳入檢視在其檢視層次結構中祖先檢視之一的檢視。示例程式碼如下:
onView(allOf(withId(R.id.myView), isDescendantOfA(withId(R.id.parentView))))
自定義檢視匹配器 (Custom View Matchers)
Espresso 提供了各種建立自定義檢視匹配器的選項,它基於 Hamcrest 匹配器。自定義匹配器是一個非常強大的概念,可以擴充套件框架,也可以根據我們的喜好自定義框架。編寫自定義匹配器的一些優點如下:
利用我們自己的自定義檢視的獨特功能
自定義匹配器有助於基於 AdapterView 的測試用例與不同型別的底層資料匹配。
透過組合多個匹配器的功能來簡化當前的匹配器
我們可以根據需要建立新的匹配器,而且非常容易。讓我們建立一個新的自定義匹配器,它返回一個匹配器來測試 TextView 的 ID 和文字。
Espresso 提供以下兩個類來編寫新的匹配器:
TypeSafeMatcher
BoundedMatcher
這兩個類本質上相似,只是 BoundedMatcher 透明地處理物件的型別轉換到正確的型別,而無需手動檢查正確的型別。我們將使用 BoundedMatcher 類建立一個新的匹配器 withIdAndText。讓我們檢查編寫新匹配器的步驟。
在 app/build.gradle 檔案中新增以下依賴項並同步。
dependencies {
implementation 'androidx.test.espresso:espresso-core:3.1.1'
}
建立一個新類來包含我們的匹配器(方法),並將其標記為 final
public final class MyMatchers {
}
在新類中宣告一個靜態方法,並使用必要的引數,並將 Matcher<View> 設定為返回型別。
public final class MyMatchers {
@NonNull
public static Matcher<View> withIdAndText(final Matcher<Integer>
integerMatcher, final Matcher<String> stringMatcher) {
}
}
在靜態方法內建立一個新的 BoundedMatcher 物件(也是返回值),其簽名如下:
public final class MyMatchers {
@NonNull
public static Matcher<View> withIdAndText(final Matcher<Integer>
integerMatcher, final Matcher<String> stringMatcher) {
return new BoundedMatcher<View, TextView>(TextView.class) {
};
}
}
重寫 BoundedMatcher 物件中的 describeTo 和 matchesSafely 方法。describeTo 只有一個 Description 型別的引數,沒有返回型別,用於提供有關匹配器的錯誤資訊。matchesSafely 有一個 TextView 型別的引數,返回型別為 boolean,用於匹配檢視。
最終版本的程式碼如下:
public final class MyMatchers {
@NonNull
public static Matcher<View> withIdAndText(final Matcher<Integer>
integerMatcher, final Matcher<String> stringMatcher) {
return new BoundedMatcher<View, TextView>(TextView.class) {
@Override
public void describeTo(final Description description) {
description.appendText("error text: ");
stringMatcher.describeTo(description);
integerMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(final TextView textView) {
return stringMatcher.matches(textView.getText().toString()) &&
integerMatcher.matches(textView.getId());
}
};
}
}
最後,我們可以使用新的匹配器來編寫測試用例,如下所示:
@Test
public void view_customMatcher_isCorrect() {
onView(withIdAndText(is((Integer) R.id.textView_hello), is((String) "Hello World!")))
.check(matches(withText("Hello World!")));
}
Espresso 測試框架 - 檢視斷言
如前所述,檢視斷言用於斷言實際檢視(使用檢視匹配器找到)和預期檢視相同。示例程式碼如下:
onView(withId(R.id.my_view)) .check(matches(withText("Hello")))
這裡:
onView() 返回與匹配檢視對應的 ViewInteration 物件。ViewInteraction 用於與匹配檢視互動。
withId(R.id.my_view) 返回一個檢視匹配器,它將與具有 id 屬性等於 my_view 的檢視(實際)匹配。
withText(“Hello”) 也返回一個檢視匹配器,它將與具有文字屬性等於 Hello 的檢視(預期)匹配。
check 是一個方法,它接受一個 ViewAssertion 型別的引數,並使用傳入的 ViewAssertion 物件進行斷言。
matches(withText(“Hello”)) 返回一個檢視斷言,它將執行斷言實際檢視(使用 withId 找到)和預期檢視(使用 withText 找到)是否相同這項實際工作。
讓我們學習 Espresso 測試框架提供的一些用於斷言檢視物件的方法。
doesNotExist()
返回一個檢視斷言,確保檢視匹配器找不到任何匹配的檢視。
onView(withText("Hello")) .check(doesNotExist());
在這裡,測試用例確保沒有文字為 Hello 的檢視。
matches()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並與目標檢視匹配器匹配的檢視匹配。
onView(withId(R.id.textView_hello)) .check(matches(withText("Hello World!")));
在這裡,測試用例確保具有 ID R.id.textView_hello 的檢視存在並與文字為 Hello World!的目標檢視匹配。
isBottomAlignedWith()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並與目標檢視匹配器底部對齊。
onView(withId(R.id.view)) .check(isBottomAlignedWith(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並與具有 ID R.id.target_view 的檢視底部對齊。
isCompletelyAbove()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並完全位於目標檢視匹配器上方。
onView(withId(R.id.view)) .check(isCompletelyAbove(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並完全位於具有 ID R.id.target_view 的檢視上方。
isCompletelyBelow()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並完全位於目標檢視匹配器下方。
onView(withId(R.id.view)) .check(isCompletelyBelow(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並完全位於具有 ID R.id.target_view 的檢視下方。
isCompletelyLeftOf()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並完全位於目標檢視匹配器的左側。
onView(withId(R.id.view)) .check(isCompletelyLeftOf(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並完全位於具有 ID R.id.target_view 的檢視左側。
isCompletelyRightOf()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並完全位於目標檢視匹配器的右側。
onView(withId(R.id.view)) .check(isCompletelyRightOf(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並完全位於具有 ID R.id.target_view 的檢視右側。
isLeftAlignedWith()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並與目標檢視匹配器左對齊。
onView(withId(R.id.view)) .check(isLeftAlignedWith(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並與具有 ID R.id.target_view 的檢視左對齊。
isPartiallyAbove()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並部分位於目標檢視匹配器上方。
onView(withId(R.id.view)) .check(isPartiallyAbove(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並部分位於具有 ID R.id.target_view 的檢視上方。
isPartiallyBelow()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並部分位於目標檢視匹配器下方。
onView(withId(R.id.view)) .check(isPartiallyBelow(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並部分位於具有 ID R.id.target_view 的檢視下方。
isPartiallyLeftOf()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並部分位於目標檢視匹配器的左側。
onView(withId(R.id.view)) .check(isPartiallyLeftOf(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並部分位於具有 ID R.id.target_view 的檢視左側。
isPartiallyRightOf()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並部分位於目標檢視匹配器的右側。
onView(withId(R.id.view)) .check(isPartiallyRightOf(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並部分位於具有 ID R.id.target_view 的檢視右側。
isRightAlignedWith()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並與目標檢視匹配器右對齊。
onView(withId(R.id.view)) .check(isRightAlignedWith(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並與具有 ID R.id.target_view 的檢視右對齊。
isTopAlignedWith()
接受目標檢視匹配器並返回一個檢視斷言,確保檢視匹配器(實際)存在並與目標檢視匹配器頂部對齊。
onView(withId(R.id.view)) .check(isTopAlignedWith(withId(R.id.target_view)))
在這裡,測試用例確保具有 ID R.id.view 的檢視存在並與具有 ID R.id.target_view 的檢視頂部對齊。
noEllipsizedText()
返回一個檢視斷言,確保檢視層次結構不包含省略號或被截斷的文字檢視。
onView(withId(R.id.view)) .check(noEllipsizedText());
noMultilineButtons()
返回一個檢視斷言,確保檢視層次結構不包含多行按鈕。
onView(withId(R.id.view)) .check(noMultilineButtons());
noOverlaps()
返回一個檢視斷言,確保可分配給 TextView 或 ImageView 的後代物件不會相互重疊。它還有另一個選項,它接受一個目標檢視匹配器並返回一個檢視斷言,確保與目標檢視匹配的後代檢視不會重疊。
Espresso 測試框架 - 檢視操作
如前所述,檢視操作自動化 Android 應用程式中使用者可以執行的所有可能的動作。Espresso 的 onView 和“onData”提供 perform 方法,該方法接受檢視操作並呼叫/自動化測試環境中的相應使用者操作。例如,“click()”是一個檢視操作,當傳遞給 onView(R.id.myButton).perform(click()) 方法時,它將在測試環境中觸發按鈕(ID:“myButton”)的單擊事件。
在本章中,讓我們學習 Espresso 測試框架提供的檢視操作。
typeText()
typeText() 接受一個 String 型別的引數(文字)並返回一個檢視操作。返回的檢視操作將提供的文字輸入檢視。在放置文字之前,它會點選檢視一次。如果檢視已經包含文字,則內容可能被放置在任意位置。
onView(withId(R.id.text_view)).perform(typeText("Hello World!"))
typeTextIntoFocusedView()
typeTextIntoFocusedView() 與 typeText() 類似,只是它將文字放置在檢視中游標位置的右側。
onView(withId(R.id.text_view)).perform(typeTextIntoFocusedView("Hello World!"))
replaceText()
replaceText() 與 typeText() 類似,只是它替換檢視的內容。
onView(withId(R.id.text_view)).perform(typeTextIntoFocusedView("Hello World!"))
clearText()
clearText() 沒有引數,返回一個檢視操作,它將清除檢視中的文字。
onView(withId(R.id.text_view)).perform(clearText())
pressKey()
pressKey() 接受鍵碼(例如 KeyEvent.KEYCODE_ENTER)並返回一個檢視操作,它將按下與鍵碼對應的鍵。
onView(withId(R.id.text_view)).perform(typeText( "Hello World!", pressKey(KeyEvent.KEYCODE_ENTER))
pressMenuKey()
pressMenuKey() 沒有引數,返回一個檢視操作,它將按下硬體選單鍵。
onView(withId(R.id.text_view)).perform(typeText( "Hello World!", pressKey(KeyEvent.KEYCODE_ENTER), pressMenuKey())
closeSoftKeyboard()
closeSoftKeyboard() 沒有引數,返回一個檢視操作,如果鍵盤已開啟,它將關閉鍵盤。
onView(withId(R.id.text_view)).perform(typeText( "Hello World!", closeSoftKeyboard())
click()
click() 沒有引數,返回一個檢視操作,它將呼叫檢視的單擊操作。
onView(withId(R.id.button)).perform(click())
doubleClick()
doubleClick() 沒有引數,返回一個檢視操作,它將呼叫檢視的雙擊操作。
onView(withId(R.id.button)).perform(doubleClick())
longClick()
longClick() 沒有引數,返回一個檢視操作,它將呼叫檢視的長按操作。
onView(withId(R.id.button)).perform(longClick())
pressBack()
pressBack() 沒有引數,返回一個檢視操作,它將點選後退按鈕。
onView(withId(R.id.button)).perform(pressBack())
pressBackUnconditionally()
pressBackUnconditionally() 沒有引數,返回一個檢視操作,它將點選後退按鈕,並且如果後退按鈕操作退出應用程式本身,則不會丟擲異常。
onView(withId(R.id.button)).perform(pressBack())
openLink()
openLink() 方法有兩個引數。第一個引數(連結文字)型別為 Matcher,引用 HTML 錨標籤的文字。第二個引數(URL)型別為 Matcher,引用 HTML 錨標籤的 URL。此方法僅適用於 TextView。它返回一個檢視操作,該操作收集文字檢視內容中所有可用的 HTML 錨標籤,查詢與第一個引數(連結文字)和第二個引數(URL)匹配的錨標籤,最後開啟相應的 URL。讓我們考慮一個文字檢視,其內容如下:
<a href="http://www.google.com/">copyright</a>
然後,可以使用下面的測試用例開啟並測試連結:
onView(withId(R.id.text_view)).perform(openLink(is("copyright"),
is(Uri.parse("http://www.google.com/"))))
這裡,openLink 將獲取文字檢視的內容,查詢文字為“copyright”,URL 為 www.google.com 的連結,並在瀏覽器中開啟該 URL。
openLinkWithText()
openLinkWithText() 有一個引數,該引數可以是 **String** 型別或 Matcher 型別。它只是 openLink 方法的快捷方式。
onView(withId(R.id.text_view)).perform(openLinkWithText("copyright"))
openLinkWithUri()
openLinkWithUri() 有一個引數,該引數可以是 String 型別或 Matcher 型別。它只是 openLink 方法的快捷方式。
onView(withId(R.id.text_view)).perform(openLinkWithUri("http://www.google.com/"))
pressImeActionButton()
pressImeActionButton() 沒有引數,並返回一個檢視操作,該操作將執行在 android:imeOptions 配置中設定的操作。例如,如果 android:imeOptions 等於 actionNext,這將把游標移動到螢幕上下一個可用的 EditText 檢視。
onView(withId(R.id.text_view)).perform(pressImeActionButton())
scrollTo()
scrollTo() 沒有引數,並返回一個檢視操作,該操作將滾動螢幕上匹配的 scrollView。
onView(withId(R.id.scrollView)).perform(scrollTo())
swipeDown()
swipeDown() 沒有引數,並返回一個檢視操作,該操作將在螢幕上觸發向下滑動操作。
onView(withId(R.id.root)).perform(swipeDown())
swipeUp()
swipeUp() 沒有引數,並返回一個檢視操作,該操作將在螢幕上觸發向上滑動操作。
onView(withId(R.id.root)).perform(swipeUp())
swipeRight()
swipeRight() 沒有引數,並返回一個檢視操作,該操作將在螢幕上觸發向右滑動操作。
onView(withId(R.id.root)).perform(swipeRight())
swipeLeft()
swipeLeft() 沒有引數,並返回一個檢視操作,該操作將在螢幕上觸發向左滑動操作。
onView(withId(R.id.root)).perform(swipeLeft())
Espresso 測試框架 - AdapterView
AdapterView 是一種特殊的檢視,專門設計用於呈現類似資訊的集合,例如使用 Adapter 從底層資料來源獲取的產品列表和使用者聯絡人。資料來源可以是簡單的列表到複雜資料庫條目。一些從 AdapterView 派生的檢視是 ListView、GridView 和 Spinner。
AdapterView 根據底層資料來源中可用資料的數量動態地呈現使用者介面。此外,AdapterView 只呈現螢幕可用可見區域內可以呈現的最小必要資料。AdapterView 這樣做是為了節省記憶體,即使底層資料很大,也能使使用者介面看起來流暢。
分析表明,AdapterView 架構的性質使得 onView 選項及其檢視匹配器變得無關緊要,因為首先可能根本沒有呈現要測試的特定檢視。幸運的是,espresso 提供了一個方法 onData(),它接受 hamcrest 匹配器(與底層資料的型別相關)來匹配底層資料,並返回對應於匹配資料檢視的 DataInteraction 型別物件。示例程式碼如下所示:
onData(allOf(is(instanceOf(String.class)), startsWith("Apple"))).perform(click())
在這裡,onData() 匹配條目“Apple”,如果它在底層資料(陣列列表)中可用,則返回 DataInteraction 物件以與匹配的檢視(對應於“Apple”條目的 TextView)進行互動。
方法
DataInteraction 提供以下方法與檢視互動:
perform()
這接受檢視操作並觸發傳入的檢視操作。
onData(allOf(is(instanceOf(String.class)), startsWith("Apple"))).perform(click())
check()
這接受檢視斷言並檢查傳入的檢視斷言。
onData(allOf(is(instanceOf(String.class)), startsWith("Apple")))
.check(matches(withText("Apple")))
inAdapterView()
這接受檢視匹配器。它根據傳入的檢視匹配器選擇特定的 AdapterView,並返回 DataInteraction 物件以與匹配的 AdapterView 進行互動。
onData(allOf()) .inAdapterView(withId(R.id.adapter_view)) .atPosition(5) .perform(click())
atPosition()
這接受一個整數型別引數,並引用底層資料中專案的在位置。它選擇與傳入的資料位置值對應的檢視,並返回 DataInteraction 物件以與匹配的檢視進行互動。如果我們知道底層資料的正確順序,這將很有用。
onData(allOf()) .inAdapterView(withId(R.id.adapter_view)) .atPosition(5) .perform(click())
onChildView()
這接受檢視匹配器並匹配特定子檢視內的檢視。例如,我們可以與基於 AdapterView 的產品列表中的特定專案(如“購買”按鈕)進行互動。
onData(allOf(is(instanceOf(String.class)), startsWith("Apple")))
.onChildView(withId(R.id.buy_button))
.perform(click())
編寫示例應用程式
按照以下步驟編寫基於 AdapterView 的簡單應用程式,並使用 onData() 方法編寫測試用例。
啟動 Android Studio。
建立新的專案,如前所述,並將其命名為 MyFruitApp。
使用 Refactor → Migrate to AndroidX 選項選單將應用程式遷移到 AndroidX 框架。
刪除主活動中的預設設計並新增 ListView。activity_main.xml 的內容如下:
<?xml version = "1.0" encoding = "utf-8"?>
<RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
xmlns:app = "http://schemas.android.com/apk/res-auto"
xmlns:tools = "http://schemas.android.com/tools"
android:layout_width = "match_parent"
android:layout_height = "match_parent"
tools:context = ".MainActivity">
<ListView
android:id = "@+id/listView"
android:layout_width = "wrap_content"
android:layout_height = "wrap_content" />
</RelativeLayout>
新增新的佈局資源 item.xml 以指定列表檢視的專案模板。item.xml 的內容如下:
<?xml version = "1.0" encoding = "utf-8"?> <TextView xmlns:android = "http://schemas.android.com/apk/res/android" android:id = "@+id/name" android:layout_width = "fill_parent" android:layout_height = "fill_parent" android:padding = "8dp" />
現在,建立一個介面卡,其水果陣列作為底層資料,並將其設定為列表檢視。這需要在 MainActivity 的 onCreate() 中完成,如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Find fruit list view
final ListView listView = (ListView) findViewById(R.id.listView);
// Initialize fruit data
String[] fruits = new String[]{
"Apple",
"Banana",
"Cherry",
"Dates",
"Elderberry",
"Fig",
"Grapes",
"Grapefruit",
"Guava",
"Jack fruit",
"Lemon",
"Mango",
"Orange",
"Papaya",
"Pears",
"Peaches",
"Pineapple",
"Plums",
"Raspberry",
"Strawberry",
"Watermelon"
};
// Create array list of fruits
final ArrayList<String> fruitList = new ArrayList<String>();
for (int i = 0; i < fruits.length; ++i) {
fruitList.add(fruits[i]);
}
// Create Array adapter
final ArrayAdapter adapter = new ArrayAdapter(this, R.layout.item, fruitList);
// Set adapter in list view
listView.setAdapter(adapter);
}
現在,編譯程式碼並執行應用程式。My Fruit App 的螢幕截圖如下:
現在,開啟 ExampleInstrumentedTest.java 檔案並新增 ActivityTestRule,如下所示:
@Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class);
此外,確保在 app/build.gradle 中完成了測試配置:
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
新增一個新的測試用例來測試列表檢視,如下所示:
@Test
public void listView_isCorrect() {
// check list view is visible
onView(withId(R.id.listView)).check(matches(isDisplayed()));
onData(allOf(is(instanceOf(String.class)), startsWith("Apple"))).perform(click());
onData(allOf(is(instanceOf(String.class)), startsWith("Apple")))
.check(matches(withText("Apple")));
// click a child item
onData(allOf())
.inAdapterView(withId(R.id.listView))
.atPosition(10)
.perform(click());
}
最後,使用 Android Studio 的上下文選單執行測試用例,並檢查所有測試用例是否成功。
Espresso 測試框架 - WebView
WebView 是 Android 提供的一種特殊檢視,用於在應用程式內顯示網頁。WebView 沒有提供像 Chrome 和 Firefox 這樣的成熟瀏覽器應用程式的所有功能。但是,它提供了對要顯示的內容的完全控制,並公開了所有可以在網頁內呼叫的 Android 功能。它啟用 WebView 並提供了一個特殊的環境,可以使用 HTML 技術和本地功能(如相機和撥打電話)輕鬆設計 UI。此功能集使 WebView 能夠提供一種稱為 混合應用程式 的新型應用程式,其中 UI 使用 HTML 完成,業務邏輯使用 JavaScript 或透過外部 API 端點完成。
通常情況下,測試 WebView 是一項挑戰,因為它使用 HTML 技術而不是原生使用者介面/檢視來建立其使用者介面元素。Espresso 在這方面表現出色,因為它提供了一組新的 Web 匹配器和 Web 斷言,這些匹配器和斷言在設計上類似於原生檢視匹配器和檢視斷言。同時,它還透過包含基於 Web 技術的測試環境提供了一種均衡的方法。
Espresso Web 基於 WebDriver Atom 框架構建,該框架用於查詢和操作 Web 元素。Atom 類似於檢視操作。Atom 將執行網頁內的所有互動。WebDriver 公開了預定義的一組方法,如 findElement()、getElement(),用於查詢 Web 元素並返回相應的 atom(在網頁中執行操作)。
標準 Web 測試語句如下所示:
onWebView() .withElement(Atom) .perform(Atom) .check(WebAssertion)
這裡:
onWebView() - 類似於 onView(),它公開了一組用於測試 WebView 的 API。
withElement() - 使用 Atom 在網頁內查詢 Web 元素的幾種方法之一,並返回 WebInteration 物件,這類似於 ViewInteraction。
perform() - 使用 Atom 在網頁內執行操作並返回 WebInteraction。
check() - 使用 WebAssertion 執行必要的斷言。
一個示例 Web 測試程式碼如下所示:
onWebView()
.withElement(findElement(Locator.ID, "apple"))
.check(webMatches(getText(), containsString("Apple")))
這裡:
findElement() 定位元素並返回一個 Atom
webMatches 類似於 matches 方法
編寫示例應用程式
讓我們編寫一個基於 WebView 的簡單應用程式,並使用 onWebView() 方法編寫測試用例。請按照以下步驟編寫示例應用程式:
啟動 Android Studio。
建立新的專案,如前所述,並將其命名為 MyWebViewApp。
使用 Refactor → Migrate to AndroidX 選項選單將應用程式遷移到 AndroidX 框架。
在 AndroidManifest.xml 檔案中新增以下配置選項以授予訪問 Internet 的許可權。
<uses-permission android:name = "android.permission.INTERNET" />
Espresso Web 作為單獨的外掛提供。因此,請在 app/build.gradle 中新增依賴項並同步。
dependencies {
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.1'
}
刪除主活動中的預設設計並新增 WebView。activity_main.xml 的內容如下:
<?xml version = "1.0" encoding = "utf-8"?>
<RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
xmlns:app = "http://schemas.android.com/apk/res-auto"
xmlns:tools = "http://schemas.android.com/tools"
android:layout_width = "match_parent"
android:layout_height = "match_parent"
tools:context = ".MainActivity">
<WebView
android:id = "@+id/web_view_test"
android:layout_width = "fill_parent"
android:layout_height = "fill_parent" />
</RelativeLayout>
建立一個新類 ExtendedWebViewClient,擴充套件 WebViewClient 並覆蓋 shouldOverrideUrlLoading 方法,以便在同一個 WebView 中載入連結操作;否則,它將在應用程式外部開啟一個新的瀏覽器視窗。將其放在 MainActivity.java 中。
private class ExtendedWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
}
現在,在 MainActivity 的 onCreate 方法中新增以下程式碼。程式碼的目的是查詢 WebView,正確配置它,然後最後載入目標 URL。
// Find web view
WebView webView = (WebView) findViewById(R.id.web_view_test);
// set web view client
webView.setWebViewClient(new ExtendedWebViewClient());
// Clear cache
webView.clearCache(true);
// load Url
webView.loadUrl("http://<your domain or IP>/index.html");
這裡:
index.html 的內容如下:
<html>
<head>
<title>Android Web View Sample</title>
</head>
<body>
<h1>Fruits</h1>
<ol>
<li><a href = "apple.html" id = "apple">Apple</a></li>
<li><a href = "banana.html" id = "banana">Banana</a></li>
</ol>
</body>
</html>
index.html 中引用的 apple.html 檔案的內容如下:
<html>
<head>
<title>Android Web View Sample</title>
</head>
<body>
<h1>Apple</h1>
</body>
</html>
banana.html 檔案中引用的 banana.html 檔案的內容如下:
<html>
<head>
<title>Android Web View Sample</title>
</head>
<body>
<h1>Banana</h1>
</body>
</html>
將 index.html、apple.html 和 banana.html 放置在 Web 伺服器中
將 loadUrl 方法中的 URL 替換為您配置的 URL。
現在,執行應用程式並手動檢查一切是否正常。以下是 WebView 示例應用程式 的螢幕截圖:
現在,開啟 ExampleInstrumentedTest.java 檔案並新增以下規則:
@Rule
public ActivityTestRule<MainActivity> mActivityRule =
new ActivityTestRule<MainActivity>(MainActivity.class, false, true) {
@Override
protected void afterActivityLaunched() {
onWebView(withId(R.id.web_view_test)).forceJavascriptEnabled();
}
};
在這裡,我們找到了 WebView 並啟用了 WebView 的 JavaScript,因為 espresso Web 測試框架專門透過 JavaScript 引擎來識別和操作 Web 元素。
現在,新增測試用例來測試我們的 WebView 及其行為。
@Test
public void webViewTest(){
onWebView()
.withElement(findElement(Locator.ID, "apple"))
.check(webMatches(getText(), containsString("Apple")))
.perform(webClick())
.withElement(findElement(Locator.TAG_NAME, "h1"))
.check(webMatches(getText(), containsString("Apple")));
}
在這裡,測試按以下順序進行:
使用 findElement() 方法和 Locator.ID 列舉透過其 id 屬性找到連結 apple。
使用 webMatches() 方法檢查連結的文字。
對連結執行單擊操作。它將開啟 apple.html 頁面。
再次使用 findElement() 方法和 Locator.TAG_NAME 列舉找到 h1 元素。
最後再次使用 webMatches() 方法檢查 h1 標籤的文字。
最後,使用 Android Studio 上下文選單執行測試用例。
非同步操作
在本章中,我們將學習如何使用 Espresso Idling Resources 測試非同步操作。
現代應用程式面臨的挑戰之一是提供流暢的使用者體驗。提供流暢的使用者體驗需要在後臺進行大量工作,以確保應用程式流程不超過幾毫秒。後臺任務範圍從簡單的任務到從遠端 API/資料庫獲取資料的複雜且耗時的任務。
如果開發多執行緒應用程式很複雜,那麼編寫其測試用例就更加複雜。例如,在從資料庫載入必要資料之前,我們不應該測試AdapterView。如果在單獨的執行緒中獲取資料,則測試需要等到執行緒完成。因此,測試環境應在後臺執行緒和 UI 執行緒之間同步。Espresso 為測試多執行緒應用程式提供了出色的支援。應用程式使用執行緒的方式如下,Espresso 支援每種場景。
使用者介面執行緒
Android SDK 內部使用它來為複雜的 UI 元素提供流暢的使用者體驗。Espresso 透明地支援此場景,不需要任何配置和特殊編碼。
非同步任務
現代程式語言支援非同步程式設計,可以在不增加執行緒程式設計複雜性的情況下進行輕量級執行緒處理。Espresso 框架也透明地支援非同步任務。
使用者執行緒
開發人員可以啟動一個新執行緒來從資料庫中獲取複雜或大型資料。為了支援這種情況,Espresso 提供了空閒資源的概念。
讓我們在本節學習空閒資源的概念以及如何使用它。
概述
空閒資源的概念非常簡單直觀。其基本思想是在單獨的執行緒中啟動長時間執行的程序時建立一個變數(布林值),以識別該程序是否正在執行,並在測試環境中註冊它。在測試期間,測試執行器將檢查註冊的變數(如果找到),然後查詢其執行狀態。如果執行狀態為真,測試執行器將等待直到狀態變為假。
Espresso 提供了一個介面IdlingResources,用於維護執行狀態。要實現的主要方法是isIdleNow()。如果isIdleNow()返回 true,Espresso 將恢復測試過程;否則,將等待直到isIdleNow()返回 false。我們需要實現IdlingResources並使用派生類。Espresso 還提供了一些內建的IdlingResources實現以減輕我們的工作量。它們如下所示:
CountingIdlingResource
這維護了一個正在執行的任務的內部計數器。它公開了increment()和decrement()方法。increment()將計數器加一,decrement()從計數器中減一。只有在沒有活動任務時,isIdleNow()才返回 true。
UriIdlingResource
這類似於CountingIdlingResource,只是計數器需要為零一段時間以考慮網路延遲。
IdlingThreadPoolExecutor
這是ThreadPoolExecutor的自定義實現,用於維護當前執行緒池中活動執行任務的數量。
IdlingScheduledThreadPoolExecutor
這類似於IdlingThreadPoolExecutor,但它也排程任務,並且是ScheduledThreadPoolExecutor的自定義實現。
如果在應用程式中使用了上述任何一個IdlingResources實現或自定義實現,則在使用IdlingRegistry類測試應用程式之前,也需要將其註冊到測試環境中,如下所示:
IdlingRegistry.getInstance().register(MyIdlingResource.getIdlingResource());
此外,測試完成後可以將其移除,如下所示:
IdlingRegistry.getInstance().unregister(MyIdlingResource.getIdlingResource());
Espresso 在單獨的包中提供此功能,需要在 app.gradle 中配置該包,如下所示:
dependencies {
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
示例應用程式
讓我們建立一個簡單的應用程式,透過在單獨的執行緒中從 Web 服務獲取水果列表,然後使用空閒資源的概念對其進行測試。
啟動 Android Studio。
建立一個新專案,如前所述,並將其命名為 MyIdlingFruitApp
使用“重構”→“遷移到 AndroidX”選項選單將應用程式遷移到 AndroidX 框架。
在 app/build.gradle 中新增 Espresso 空閒資源庫(並同步),如下所示:
dependencies {
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
刪除主活動中的預設設計並新增 ListView。activity_main.xml 的內容如下所示:
<?xml version = "1.0" encoding = "utf-8"?>
<RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
xmlns:app = "http://schemas.android.com/apk/res-auto"
xmlns:tools = "http://schemas.android.com/tools"
android:layout_width = "match_parent"
android:layout_height = "match_parent"
tools:context = ".MainActivity">
<ListView
android:id = "@+id/listView"
android:layout_width = "wrap_content"
android:layout_height = "wrap_content" />
</RelativeLayout>
新增新的佈局資源 item.xml 以指定列表檢視的專案模板。item.xml 的內容如下:
<?xml version = "1.0" encoding = "utf-8"?> <TextView xmlns:android = "http://schemas.android.com/apk/res/android" android:id = "@+id/name" android:layout_width = "fill_parent" android:layout_height = "fill_parent" android:padding = "8dp" />
建立一個新類 - MyIdlingResource。MyIdlingResource 用於在一個地方儲存我們的 IdlingResource,並在需要時獲取它。我們將在示例中使用 CountingIdlingResource。
package com.tutorialspoint.espressosamples.myidlingfruitapp;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.idling.CountingIdlingResource;
public class MyIdlingResource {
private static CountingIdlingResource mCountingIdlingResource =
new CountingIdlingResource("my_idling_resource");
public static void increment() {
mCountingIdlingResource.increment();
}
public static void decrement() {
mCountingIdlingResource.decrement();
}
public static IdlingResource getIdlingResource() {
return mCountingIdlingResource;
}
}
在 MainActivity 類中宣告一個全域性變數 mIdlingResource,型別為 CountingIdlingResource,如下所示:
@Nullable private CountingIdlingResource mIdlingResource = null;
編寫一個私有方法從 Web 獲取水果列表,如下所示:
private ArrayList<String> getFruitList(String data) {
ArrayList<String> fruits = new ArrayList<String>();
try {
// Get url from async task and set it into a local variable
URL url = new URL(data);
Log.e("URL", url.toString());
// Create new HTTP connection
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// Set HTTP connection method as "Get"
conn.setRequestMethod("GET");
// Do a http request and get the response code
int responseCode = conn.getResponseCode();
// check the response code and if success, get response content
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
StringBuffer response = new StringBuffer();
while ((line = in.readLine()) != null) {
response.append(line);
}
in.close();
JSONArray jsonArray = new JSONArray(response.toString());
Log.e("HTTPResponse", response.toString());
for(int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String name = String.valueOf(jsonObject.getString("name"));
fruits.add(name);
}
} else {
throw new IOException("Unable to fetch data from url");
}
conn.disconnect();
} catch (IOException | JSONException e) {
e.printStackTrace();
}
return fruits;
}
在 onCreate() 方法中建立一個新任務,使用我們的 getFruitList 方法從 Web 獲取資料,然後建立一個新介面卡並將其設定到列表檢視中。一旦我們的工作線上程中完成,也遞減空閒資源。程式碼如下所示:
// Get data
class FruitTask implements Runnable {
ListView listView;
CountingIdlingResource idlingResource;
FruitTask(CountingIdlingResource idlingRes, ListView listView) {
this.listView = listView;
this.idlingResource = idlingRes;
}
public void run() {
//code to do the HTTP request
final ArrayList<String> fruitList = getFruitList("http://<your domain or IP>/fruits.json");
try {
synchronized (this){
runOnUiThread(new Runnable() {
@Override
public void run() {
// Create adapter and set it to list view
final ArrayAdapter adapter = new
ArrayAdapter(MainActivity.this, R.layout.item, fruitList);
ListView listView = (ListView)findViewById(R.id.listView);
listView.setAdapter(adapter);
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
if (!MyIdlingResource.getIdlingResource().isIdleNow()) {
MyIdlingResource.decrement(); // Set app as idle.
}
}
}
此處,水果 URL 被視為 http://<your domain or IP/fruits.json,並格式化為 JSON。內容如下:
[
{
"name":"Apple"
},
{
"name":"Banana"
},
{
"name":"Cherry"
},
{
"name":"Dates"
},
{
"name":"Elderberry"
},
{
"name":"Fig"
},
{
"name":"Grapes"
},
{
"name":"Grapefruit"
},
{
"name":"Guava"
},
{
"name":"Jack fruit"
},
{
"name":"Lemon"
},
{
"name":"Mango"
},
{
"name":"Orange"
},
{
"name":"Papaya"
},
{
"name":"Pears"
},
{
"name":"Peaches"
},
{
"name":"Pineapple"
},
{
"name":"Plums"
},
{
"name":"Raspberry"
},
{
"name":"Strawberry"
},
{
"name":"Watermelon"
}
]
注意 - 將檔案放在本地 Web 伺服器上並使用它。
現在,查詢檢視,透過傳遞 FruitTask 建立一個新執行緒,遞增空閒資源,最後啟動任務。
// Find list view ListView listView = (ListView) findViewById(R.id.listView); Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView)); MyIdlingResource.increment(); fruitTask.start();
MainActivity 的完整程式碼如下所示:
package com.tutorialspoint.espressosamples.myidlingfruitapp;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatActivity;
import androidx.test.espresso.idling.CountingIdlingResource;
import android.os.Bundle;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
@Nullable
private CountingIdlingResource mIdlingResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Get data
class FruitTask implements Runnable {
ListView listView;
CountingIdlingResource idlingResource;
FruitTask(CountingIdlingResource idlingRes, ListView listView) {
this.listView = listView;
this.idlingResource = idlingRes;
}
public void run() {
//code to do the HTTP request
final ArrayList<String> fruitList = getFruitList(
"http://<yourdomain or IP>/fruits.json");
try {
synchronized (this){
runOnUiThread(new Runnable() {
@Override
public void run() {
// Create adapter and set it to list view
final ArrayAdapter adapter = new ArrayAdapter(
MainActivity.this, R.layout.item, fruitList);
ListView listView = (ListView) findViewById(R.id.listView);
listView.setAdapter(adapter);
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
if (!MyIdlingResource.getIdlingResource().isIdleNow()) {
MyIdlingResource.decrement(); // Set app as idle.
}
}
}
// Find list view
ListView listView = (ListView) findViewById(R.id.listView);
Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView));
MyIdlingResource.increment();
fruitTask.start();
}
private ArrayList<String> getFruitList(String data) {
ArrayList<String> fruits = new ArrayList<String>();
try {
// Get url from async task and set it into a local variable
URL url = new URL(data);
Log.e("URL", url.toString());
// Create new HTTP connection
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// Set HTTP connection method as "Get"
conn.setRequestMethod("GET");
// Do a http request and get the response code
int responseCode = conn.getResponseCode();
// check the response code and if success, get response content
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
StringBuffer response = new StringBuffer();
while ((line = in.readLine()) != null) {
response.append(line);
}
in.close();
JSONArray jsonArray = new JSONArray(response.toString());
Log.e("HTTPResponse", response.toString());
for(int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String name = String.valueOf(jsonObject.getString("name"));
fruits.add(name);
}
} else {
throw new IOException("Unable to fetch data from url");
}
conn.disconnect();
} catch (IOException | JSONException e) {
e.printStackTrace();
}
return fruits;
}
}
現在,在應用程式清單檔案 AndroidManifest.xml 中新增以下配置
<uses-permission android:name = "android.permission.INTERNET" />
現在,編譯上述程式碼並執行應用程式。My Idling Fruit App 的螢幕截圖如下所示:
現在,開啟 ExampleInstrumentedTest.java 檔案並新增 ActivityTestRule,如下所示:
@Rule
public ActivityTestRule<MainActivity> mActivityRule =
new ActivityTestRule<MainActivity>(MainActivity.class);
Also, make sure the test configuration is done in app/build.gradle
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
新增一個新的測試用例來測試列表檢視,如下所示:
@Before
public void registerIdlingResource() {
IdlingRegistry.getInstance().register(MyIdlingResource.getIdlingResource());
}
@Test
public void contentTest() {
// click a child item
onData(allOf())
.inAdapterView(withId(R.id.listView))
.atPosition(10)
.perform(click());
}
@After
public void unregisterIdlingResource() {
IdlingRegistry.getInstance().unregister(MyIdlingResource.getIdlingResource());
}
最後,使用 Android Studio 的上下文選單執行測試用例,並檢查所有測試用例是否成功。
Espresso 測試框架 - Intents
Android Intent 用於開啟新的活動,無論是內部的(例如從產品列表螢幕開啟產品詳情螢幕)還是外部的(例如開啟撥號器進行撥打電話)。Espresso 測試框架透明地處理內部 Intent 活動,不需要使用者進行任何特定操作。但是,呼叫外部活動確實是一個挑戰,因為它超出了我們的範圍,即被測試的應用程式。一旦使用者呼叫外部應用程式並離開被測試的應用程式,使用者以預定義的動作順序返回應用程式的可能性就比較小。因此,我們需要在測試應用程式之前假設使用者操作。Espresso 提供了兩種處理這種情況的選項。它們如下所示:
intended()
這允許使用者確保從被測試應用程式中打開了正確的 Intent。
intending()
這允許使用者模擬外部活動,例如從相機拍照、從聯絡人列表撥打電話等,並使用預定義的值集返回應用程式(例如,使用預定義的影像而不是實際影像)。
設定
Espresso 透過外掛庫支援 Intent 選項,需要在應用程式的 gradle 檔案中配置該庫。配置選項如下所示:
dependencies {
// ...
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1'
}
intended()
Espresso Intent 外掛提供特殊的匹配器來檢查呼叫的 Intent 是否是預期的 Intent。提供的匹配器及其用途如下所示:
hasAction
這接受 Intent 動作並返回一個匹配器,該匹配器匹配指定的 Intent。
hasData
這接受資料並返回一個匹配器,該匹配器匹配呼叫時提供給 Intent 的資料。
toPackage
這接受 Intent 包名並返回一個匹配器,該匹配器匹配呼叫的 Intent 的包名。
現在,讓我們建立一個新應用程式並使用 intended() 測試外部活動的應用程式,以瞭解該概念。
啟動 Android Studio。
建立一個新專案,如前所述,並將其命名為 IntentSampleApp。
使用“重構”→“遷移到 AndroidX”選項選單將應用程式遷移到 AndroidX 框架。
建立一個文字框,一個開啟聯絡人列表的按鈕,另一個用於撥打電話的按鈕,方法是更改 activity_main.xml,如下所示:
<?xml version = "1.0" encoding = "utf-8"?>
<RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
xmlns:app = "http://schemas.android.com/apk/res-auto"
xmlns:tools = "http://schemas.android.com/tools"
android:layout_width = "match_parent"
android:layout_height = "match_parent"
tools:context = ".MainActivity">
<EditText
android:id = "@+id/edit_text_phone_number"
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
android:layout_centerHorizontal = "true"
android:text = ""
android:autofillHints = "@string/phone_number"/>
<Button
android:id = "@+id/call_contact_button"
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
android:layout_centerHorizontal = "true"
android:layout_below = "@id/edit_text_phone_number"
android:text = "@string/call_contact"/>
<Button
android:id = "@+id/button"
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
android:layout_centerHorizontal = "true"
android:layout_below = "@id/call_contact_button"
android:text = "@string/call"/>
</RelativeLayout>
還在 strings.xml 資原始檔中新增以下專案:
<string name = "phone_number">Phone number</string> <string name = "call">Call</string> <string name = "call_contact">Select from contact list</string>
現在,在 onCreate 方法下的主活動 (MainActivity.java) 中新增以下程式碼。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... code
// Find call from contact button
Button contactButton = (Button) findViewById(R.id.call_contact_button);
contactButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Uri uri = Uri.parse("content://contacts");
Intent contactIntent = new Intent(Intent.ACTION_PICK,
ContactsContract.Contacts.CONTENT_URI);
contactIntent.setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE);
startActivityForResult(contactIntent, REQUEST_CODE);
}
});
// Find edit view
final EditText phoneNumberEditView = (EditText)
findViewById(R.id.edit_text_phone_number);
// Find call button
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(phoneNumberEditView.getText() != null) {
Uri number = Uri.parse("tel:" + phoneNumberEditView.getText());
Intent callIntent = new Intent(Intent.ACTION_DIAL, number);
startActivity(callIntent);
}
}
});
}
// ... code
}
在這裡,我們為 id 為 call_contact_button 的按鈕編寫了程式,以開啟聯絡人列表,併為 id 為 button 的按鈕編寫了程式,以撥打電話。
在 MainActivity 類中新增一個靜態變數 REQUEST_CODE,如下所示:
public class MainActivity extends AppCompatActivity {
// ...
private static final int REQUEST_CODE = 1;
// ...
}
現在,在 MainActivity 類中新增 onActivityResult 方法,如下所示:
public class MainActivity extends AppCompatActivity {
// ...
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE) {
if (resultCode == RESULT_OK) {
// Bundle extras = data.getExtras();
// String phoneNumber = extras.get("data").toString();
Uri uri = data.getData();
Log.e("ACT_RES", uri.toString());
String[] projection = {
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME };
Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
cursor.moveToFirst();
int numberColumnIndex =
cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
String number = cursor.getString(numberColumnIndex);
int nameColumnIndex = cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
String name = cursor.getString(nameColumnIndex);
Log.d("MAIN_ACTIVITY", "Selected number : " + number +" , name : "+name);
// Find edit view
final EditText phoneNumberEditView = (EditText)
findViewById(R.id.edit_text_phone_number);
phoneNumberEditView.setText(number);
}
}
};
// ...
}
在這裡,當用戶使用 call_contact_button 按鈕開啟聯絡人列表並選擇聯絡人後返回應用程式時,將呼叫 onActivityResult。一旦呼叫 onActivityResult 方法,它將獲取使用者選擇的聯絡人,查詢聯絡電話號碼並將其設定為文字框。
執行應用程式並確保一切正常。Intent 示例應用程式的最終外觀如下所示:
現在,在應用程式的 gradle 檔案中配置 Espresso Intent,如下所示:
dependencies {
// ...
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1'
}
單擊 Android Studio 提供的“立即同步”選單選項。這將下載 Intent 測試庫並正確配置它。
開啟 ExampleInstrumentedTest.java 檔案,並新增 IntentsTestRule 來代替通常使用的 AndroidTestRule。IntentTestRule 是一個處理 Intent 測試的特殊規則。
public class ExampleInstrumentedTest {
// ... code
@Rule
public IntentsTestRule<MainActivity> mActivityRule =
new IntentsTestRule<>(MainActivity.class);
// ... code
}
新增兩個區域性變數來設定測試電話號碼和撥號程式包名稱,如下所示:
public class ExampleInstrumentedTest {
// ... code
private static final String PHONE_NUMBER = "1 234-567-890";
private static final String DIALER_PACKAGE_NAME = "com.google.android.dialer";
// ... code
}
使用 Android Studio 提供的 Alt + Enter 選項修復匯入問題,或者包含以下匯入語句:
import android.content.Context; import android.content.Intent; import androidx.test.InstrumentationRegistry; import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.runner.AndroidJUnit4; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.intent.Intents.intended; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData; import static androidx.test.espresso.intent.matcher.IntentMatchers.toPackage; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static org.hamcrest.core.AllOf.allOf; import static org.junit.Assert.*;
新增以下測試用例以測試撥號器是否已正確呼叫:
public class ExampleInstrumentedTest {
// ... code
@Test
public void validateIntentTest() {
onView(withId(R.id.edit_text_phone_number))
.perform(typeText(PHONE_NUMBER), closeSoftKeyboard());
onView(withId(R.id.button)) .perform(click());
intended(allOf(
hasAction(Intent.ACTION_DIAL),
hasData("tel:" + PHONE_NUMBER),
toPackage(DIALER_PACKAGE_NAME)));
}
// ... code
}
在這裡,hasAction、hasData 和 toPackage 匹配器與 allOf 匹配器一起使用,只有在所有匹配器都透過時才成功。
現在,透過 Android Studio 中的上下文選單執行 ExampleInstrumentedTest。
intending()
Espresso 提供了一個特殊方法 - intending() 來模擬外部 Intent 操作。intending() 接受要模擬的 Intent 的包名,並提供一個方法 respondWith 來設定如何使用模擬的 Intent 進行響應,如下所示:
intending(toPackage("com.android.contacts")).respondWith(result);
在這裡,respondWith() 接受型別為 Instrumentation.ActivityResult 的 Intent 結果。我們可以建立新的存根 Intent 並手動設定結果,如下所示:
// Stub intent
Intent intent = new Intent();
intent.setData(Uri.parse("content://com.android.contacts/data/1"));
Instrumentation.ActivityResult result =
new Instrumentation.ActivityResult(Activity.RESULT_OK, intent);
測試是否正確打開了聯絡人應用程式的完整程式碼如下所示:
@Test
public void stubIntentTest() {
// Stub intent
Intent intent = new Intent();
intent.setData(Uri.parse("content://com.android.contacts/data/1"));
Instrumentation.ActivityResult result =
new Instrumentation.ActivityResult(Activity.RESULT_OK, intent);
intending(toPackage("com.android.contacts")).respondWith(result);
// find the button and perform click action
onView(withId(R.id.call_contact_button)).perform(click());
// get context
Context targetContext2 = InstrumentationRegistry.getInstrumentation().getTargetContext();
// get phone number
String[] projection = { ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME };
Cursor cursor =
targetContext2.getContentResolver().query(Uri.parse("content://com.android.cont
acts/data/1"), projection, null, null, null);
cursor.moveToFirst();
int numberColumnIndex =
cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
String number = cursor.getString(numberColumnIndex);
// now, check the data
onView(withId(R.id.edit_text_phone_number))
.check(matches(withText(number)));
}
在這裡,我們建立了一個新的意圖,並將返回值(呼叫意圖時)設定為聯絡人列表的第一個條目,content://com.android.contacts/data/1。然後,我們設定了intending方法來模擬新建立的意圖以代替聯絡人列表。當呼叫包com.android.contacts時,它設定並呼叫我們新建立的意圖,並返回列表的預設第一個條目。然後,我們觸發了click()操作來啟動模擬意圖,最後檢查呼叫模擬意圖的電話號碼與聯絡人列表中第一個條目的號碼是否相同。
如果存在任何缺少匯入的問題,則使用Android Studio提供的Alt + Enter選項修復這些匯入問題,或者包含以下匯入語句:
import android.app.Activity; import android.app.Instrumentation; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; import androidx.test.InstrumentationRegistry; import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.runner.AndroidJUnit4; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.intent.Intents.intended; import static androidx.test.espresso.intent.Intents.intending; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData; import static androidx.test.espresso.intent.matcher.IntentMatchers.toPackage; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.core.AllOf.allOf; import static org.junit.Assert.*;
在測試類中新增以下規則,以提供讀取聯絡人列表的許可權:
@Rule public GrantPermissionRule permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS);
在應用程式清單檔案AndroidManifest.xml中新增以下選項:
<uses-permission android:name = "android.permission.READ_CONTACTS" />
現在,確保聯絡人列表至少有一個條目,然後使用Android Studio的上下文選單執行測試。
多個應用程式的UI
Android支援涉及多個應用程式的使用者介面測試。讓我們假設我們的應用程式有一個選項可以從我們的應用程式移動到訊息應用程式以傳送訊息,然後返回到我們的應用程式。在這種情況下,UI自動化測試框架可以幫助我們測試應用程式。UI自動化可以被認為是Espresso測試框架的良好補充。在選擇UI自動化之前,我們可以利用Espresso測試框架中的intending()選項。
設定說明
Android 提供 UI 自動化作為單獨的外掛。需要按照以下說明在app/build.gradle中進行配置:
dependencies {
...
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}
編寫測試用例的工作流程
讓我們瞭解如何編寫基於UI Automator的測試用例:
透過呼叫getInstance()方法並傳遞Instrumentation物件來獲取UiDevice物件。
myDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); myDevice.pressHome();
使用findObject()方法獲取UiObject物件。在使用此方法之前,我們可以開啟uiautomatorviewer應用程式來檢查目標應用程式的UI元件,因為了解目標應用程式使我們能夠編寫更好的測試用例。
UiObject button = myDevice.findObject(new UiSelector()
.text("Run")
.className("android.widget.Button"));
透過呼叫UiObject的方法來模擬使用者互動。例如,使用setText()編輯文字欄位,使用click()觸發按鈕的點選事件。
if(button.exists() && button.isEnabled()) {
button.click();
}
最後,我們檢查UI是否反映預期狀態。
Espresso 測試框架 - 測試錄製器
編寫測試用例是一項繁瑣的工作。即使Espresso提供了非常簡單靈活的API,編寫測試用例也可能是一項費力且耗時的任務。為了克服這個問題,Android Studio提供了一個錄製和生成Espresso測試用例的功能。“錄製Espresso測試”位於“執行”選單下。
讓我們按照以下步驟在我們的HelloWorldApp中錄製一個簡單的測試用例:
開啟Android Studio以及HelloWorldApp應用程式。
點選執行→錄製Espresso測試並選擇MainActivity。
錄製器螢幕截圖如下:
點選新增斷言。它將開啟如下所示的應用程式螢幕:
點選Hello World!。錄製器螢幕轉到選擇文字檢視如下所示:
再次點選儲存斷言,這將儲存斷言並顯示如下:
點選確定。它將開啟一個新視窗並詢問測試用例的名稱。預設名稱為MainActivityTest
如有必要,更改測試用例名稱。
再次點選確定。這將生成一個包含我們錄製測試用例的檔案MainActivityTest。完整的程式碼如下:
package com.tutorialspoint.espressosamples.helloworldapp;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.espresso.ViewInteraction;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void mainActivityTest() {
ViewInteraction textView = onView(
allOf(withId(R.id.textView_hello), withText("Hello World!"),
childAtPosition(childAtPosition(withId(android.R.id.content),
0),0),isDisplayed()));
textView.check(matches(withText("Hello World!")));
}
private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup &&
parentMatcher.matches(parent)&& view.equals(((ViewGroup)
parent).getChildAt(position));
}
};
}
}
最後,使用上下文選單執行測試並檢查測試用例是否執行。
Espresso 測試框架 - UI 效能
積極的使用者體驗在應用程式的成功中起著非常重要的作用。使用者體驗不僅包括漂亮的使用者介面,還包括這些漂亮的使用者介面的渲染速度以及每秒幀數。使用者介面需要以每秒60幀的速度持續執行才能提供良好的使用者體驗。
讓我們在本節中學習Android中一些用於分析UI效能的選項。
dumpsys
dumpsys是Android裝置中內建的工具。它輸出有關係統服務的當前資訊。dumpsys可以選擇轉儲特定類別的資訊。傳遞gfxinfo將提供所提供包的動畫資訊。命令如下:
> adb shell dumpsys gfxinfo <PACKAGE_NAME>
framestats
framestats是dumpsys命令的一個選項。一旦使用framestats呼叫dumpsys,它將轉儲最近幀的詳細幀計時資訊。命令如下:
> adb shell dumpsys gfxinfo <PACKAGE_NAME> framestats
它以CSV(逗號分隔值)格式輸出資訊。CSV格式的輸出有助於輕鬆地將資料推送到Excel中,並隨後透過Excel公式和圖表提取有用的資訊。
systrace
systrace也是Android裝置中內建的工具。它捕獲並顯示應用程式程序的執行時間。可以使用Android Studio終端中的以下命令執行systrace:
python %ANDROID_HOME%/platform-tools/systrace/systrace.py --time=10 -o my_trace_output.html gfx view res
Espresso 測試框架 - 可訪問性
可訪問性功能是任何應用程式的關鍵功能之一。供應商開發的應用程式應支援Android SDK設定的最低可訪問性指南,才能成為一個成功且有用的應用程式。遵循可訪問性標準非常重要,但這並非易事。Android SDK透過提供設計良好的檢視來建立可訪問的使用者介面,提供了極大的支援。
同樣,Espresso測試框架透過將可訪問性測試功能透明地支援到核心測試引擎中,為開發人員和終端使用者提供了極大的幫助。
在Espresso中,開發人員可以透過AccessibilityChecks類啟用和配置可訪問性測試。示例程式碼如下:
AccessibilityChecks.enable();
預設情況下,當您執行任何檢視操作時,都會執行可訪問性檢查。檢查包括執行操作的檢視以及所有後代檢視。您可以使用以下程式碼檢查螢幕的整個檢視層次結構:
AccessibilityChecks.enable().setRunChecksFromRootView(true);
結論
Espresso 是一個非常棒的工具,Android 開發人員可以使用它以非常簡單的方式完全測試他們的應用程式,而無需付出測試框架通常需要的額外努力。它甚至具有錄製器,可以建立測試用例而無需手動編寫程式碼。此外,它支援所有型別使用者介面測試。透過使用Espresso測試框架,Android開發人員可以自信地開發一個外觀精美且成功的應用程式,而無需在短時間內出現任何問題。