前言
想想接觸Android也有三年多的時間了,實際開發(fā)也有兩年的時間了,好像也很少接觸到Android自動化測試,雖然偶有聽說,但也沒有認(rèn)真的學(xué)習(xí)過。相信很多朋友跟我也有一樣的經(jīng)歷,對自動化測試不了解,加上項目沒有要求,認(rèn)為自動化測試價值不高,完全是浪費時間。但實際的情況并不是這樣,前段時間聽一個朋友講了些Android自動化測試,給了我很深的印象。正因為這樣的契機,所以前段時間也花時間學(xué)了Android基本的自動化測試。趁最近剛好有空整理了一下自己的學(xué)習(xí)心得。
大家可以看一下這篇文章,可能會說服你:為什么要進(jìn)行煩人的單元測試?
Android Testing Support Library
在2015年Google I/O大會上,Google放出了一個Android Testing Support Library,該庫提供了大量用于測試 Android 應(yīng)用的框架。此庫提供了一組 API,讓您可以為應(yīng)用快速構(gòu)建何運行測試代碼,包括單元測試 JUnit 4 和功能性用戶界面 (UI) 測試。我們可以從Android Studio IDE或命令行運行使用這些 API 創(chuàng)建的測試。 在測試庫中包含AndroidJUnitRunner類是一個JUnit測試運行器,可讓我們在 Android 設(shè)備上運行 JUnit 3 或 JUnit 4 樣式測試類,包括使用Espresso和UI Automator測試框架的設(shè)備。測試運行器可以將測試軟件包和要測試的應(yīng)用加載到設(shè)備、運行測試并報告測試結(jié)果。所以后面會講到的單元測試和UI測試的詳細(xì)使用,都是基于Android Testing Support Library。
單元測試 JUnit 4
我們在實際項目開發(fā)中的時候,都是需要寫成千上萬個方法或函數(shù),這些函數(shù)的功能可能很強大,也可能是很小一個功能,但我們在程序中使用時都是需要經(jīng)過測試的,保證這一部分功能是正確的。所以說,每編寫完一個函數(shù)之后,都應(yīng)該對這個函數(shù)的方方面面進(jìn)行測試,這樣的測試我們稱之為單元測試。傳統(tǒng)的編程方式,進(jìn)行單元測試是一件很麻煩的事情,我們需要在該程序中調(diào)用你需要測試的方法,并且仔細(xì)觀察運行結(jié)果,看看是否有錯。正因為如此麻煩,所以就有了很多單元測試框架,JUnit 4就是其中一種。
本地單元測試 Local Unit Tests
這種測試運行在本地開發(fā)環(huán)境的Java虛擬機上,也不需要連接Android設(shè)備或者模擬器,因此并無法獲得Android相關(guān)的API,所以只能測試只使用Java API的一些功能。
測試類代碼編寫也很簡單,主要通過一些注解來標(biāo)示,同時可以通過assertXXXX來斷言結(jié)果
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 4);
}
}
Junit 4注解
- @Before標(biāo)注setup方法,每個單元測試用例方法調(diào)用之前都會調(diào)用
- @After標(biāo)注teardown方法,每個單元測試用例方法調(diào)用之后都會調(diào)用
- @Test標(biāo)注的每個方法都是一個測試用例
- @BeforeClass標(biāo)注的靜態(tài)方法,在當(dāng)前測試類所有用例方法執(zhí)行之前執(zhí)行
- @AfterClass標(biāo)注的靜態(tài)方法,在當(dāng)前測試類所有用例方法執(zhí)行之后執(zhí)行
- @Test(timeout=)為測試用例指定超時時間
斷言
Junit提供了一系列斷言來判斷是pass還是fail
- assertTrue(condition):condition為真pass,否則fail
- assertFalse(condition):condition為假pass,否則fail
- fail():直接fail
- assertEquals(expected, actual):expected equal actual pass,否則fail
- assertSame(expected, actual):expected == actual pass,否則fail
設(shè)備單元測試 Instrumented Unit Tests
這種測試方式需要連接Android設(shè)備或模擬器??梢岳肁ndroid框架API,比如測試需要訪問設(shè)備信息(如目標(biāo)應(yīng)用程序的上下文中)或如果他們需要一個Android 相關(guān)的API(如Parcelable或SharedPreferences對象)。在使用上也很簡單,相比本地單元測試該測試類必須以 @RunWith(AndroidJUnit4.class) 注解作為前綴。
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("top.qingningshe.test", appContext.getPackageName());
}
}
相比Local Unit Tests 多了訪問設(shè)備信息、測試篩選
訪問設(shè)備信息
我們可以使用 InstrumentationRegistry
類訪問與測試運行相關(guān)的信息。此類包括 Instrumentation對象、目標(biāo)應(yīng)用Context對象、測試應(yīng)用Context對象,以及傳遞到測試中的命令行參數(shù)。
測試篩選
- @RequiresDevice:指定測試僅在物理設(shè)備而不在模擬器上運行。
- @SdkSupress:禁止在低于給定級別的 Android API 級別上運行測試。例如,要禁止在低于 18 的所有 API 級別上運行測試,請使用注解 @SDKSupress(minSdkVersion=18)。
- @SmallTest、@MediumTest和@LargeTest:指定測試的運行時長以及運行頻率。
UI測試
Espresso
Espresso 測試框架提供了一組 API 來構(gòu)建 UI 測試,用于測試應(yīng)用中的用戶流。利用這些 API,您可以編寫簡潔、運行可靠的自動化 UI 測試。Espresso 非常適合編寫白盒自動化測試,其中測試代碼將利用所測試應(yīng)用的實現(xiàn)代碼詳情。
Espresso 測試框架的主要功能包括:
- 靈活的 API,用于目標(biāo)應(yīng)用中的視圖和適配器匹配。
- 一組豐富的操作 API,用于自動化 UI 交互。
- UI 線程同步,用于提升測試可靠性。
要求 Android 2.2(API 級別 8)或更高版本。
視圖匹配
利用Espresso.onView()方法,您可以訪問目標(biāo)應(yīng)用中的 UI 組件并與之交互。此方法接受Matcher參數(shù)并搜索視圖層次結(jié)構(gòu),以找到符合給定條件的相應(yīng)View實例。您可以通過指定以下條件來優(yōu)化搜索:
- 視圖的類名稱 onView(withClassName());
- 視圖的內(nèi)容描述 onView(withContentDescription());
- 視圖的ID onView(withId());
- 在視圖中顯示的文本 onView(withText());
更多的可以查看ViewMatchers。如果搜索成功,onView()方法將返回一個引用,讓您可以執(zhí)行用戶操作并基于目標(biāo)視圖對斷言進(jìn)行測試。
適配器匹配
在AdapterView布局中,布局在運行時由子視圖動態(tài)填充。如果目標(biāo)視圖位于某個布局內(nèi)部,而該布局是從AdapterView(例如ListView或GridView)派生出的子類,則onView()方法可能無法工作,因為只有布局視圖的子集會加載到當(dāng)前視圖層次結(jié)構(gòu)中。因此,需要使用Espresso.onData()方法訪問目標(biāo)視圖元素。Espresso.onData()方法將返回一個引用,讓您可以執(zhí)行用戶操作并根據(jù)AdapterView中的元素對斷言進(jìn)行測試。
//點擊spinner
onView(withId(R.id.spinner)).perform(click());
//點擊adpaterviewer中類型為String 并且內(nèi)容為test的文本,
onData(allOf(is(instanceOf(String.class)),is("test"))).perform(click());
操作API
在上面的一段代碼中,我們用到了perform(click()),那么除了click()方法還有其他功能強大的方法可以供我們使用,下面列舉一些常用的方法:
- click():返回一個點擊動作,Espresso利用這個方法執(zhí)行一次點擊操作,就和我們自己手動點擊按鈕一樣。
- clearText():返回一個清除指定view中的文本action,在測試EditText時用的比較多。
- swipeLeft():返回一個從右往左滑動的action,這個在測試ViewPager時特別有用。
- swipeRight():返回一個從左往右滑動的action,這個在測試ViewPager時特別有用。
- swipeDown():返回一個從上往下滑動的action。
- swipeUp():返回一個從下往上滑動的action。
- closeSoftKeyboard():返回一個關(guān)閉輸入鍵盤的action。
- pressBack():返回一個點擊手機上返回鍵的action。
- doubleClick():返回一個雙擊action
- longClick():返回一個長按action
更多的可以查看ViewActions。
校驗結(jié)果
調(diào)用ViewInteraction.check()和DataInteraction.check()方法,可以判斷UI元素的狀態(tài),如果斷言失敗,會拋出AssertionFailedError異常。
- doesNotExist:斷言某一個view不存在。
- matches:斷言某個view存在,且符合一列的匹配。
- selectedDescendentsMatch:斷言指定的子元素存在,且他們的狀態(tài)符合一些列的匹配。
onView(withId(R.id.textview)).check(matches(withText("test")));
UI 線程同步
Espresso 的核心是它可以與待測應(yīng)用無縫同步測試操作的能力。默認(rèn)情況下,Espresso 會等待當(dāng)前消息隊列中的 UI 事件執(zhí)行(默認(rèn)是 AsyncTask)完畢再進(jìn)行下一個測試操作。這應(yīng)該能解決大部分應(yīng)用與測試同步的問題。然而,應(yīng)用中有一些執(zhí)行后臺操作的對象(比如與網(wǎng)絡(luò)服務(wù)交互)通過非標(biāo)準(zhǔn)方式實現(xiàn);例如:直接創(chuàng)建和管理線程,以及使用自定義服務(wù)。慶幸的是 Espresso 仍然可以同步測試操作與你的自定義資源。
以下是我們需要完成的:
- 實現(xiàn) ?IdlingResource 接口并暴露給測試。
- 通過調(diào)用ResourceCallback..onTransitionToIdle()通知Espresso。
需要注意的是 IdlingResource 接口是在待測應(yīng)用中實現(xiàn)的,所以你需要添加依賴:
compile 'com.android.support.test.espresso:espresso-idling-resource:2.2.2'
下面我們看看官方的例子是如何實現(xiàn)的
public final class CountingIdlingResource implements IdlingResource {
private static final String TAG = "CountingIdlingResource";
private final String resourceName;
private final AtomicInteger counter = new AtomicInteger(0);
private final boolean debugCounting;
// written from main thread, read from any thread.
private volatile ResourceCallback resourceCallback;
// read/written from any thread - used for debugging messages.
private volatile long becameBusyAt = 0;
private volatile long becameIdleAt = 0;
/**
* Creates a CountingIdlingResource without debug tracing.
*
* @param resourceName the resource name this resource should report to Espresso.
*/
public CountingIdlingResource(String resourceName) {
this(resourceName, false);
}
/**
* Creates a CountingIdlingResource.
*
* @param resourceName the resource name this resource should report to Espresso.
* @param debugCounting if true increment & decrement calls will print trace information to logs.
*/
public CountingIdlingResource(String resourceName, boolean debugCounting) {
this.resourceName = checkNotNull(resourceName);
this.debugCounting = debugCounting;
}
@Override
public String getName() {
return resourceName;
}
@Override
public boolean isIdleNow() {
return counter.get() == 0;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
/**
* Increments the count of in-flight transactions to the resource being monitored.
*
* This method can be called from any thread.
*/
public void increment() {
int counterVal = counter.getAndIncrement();
if (0 == counterVal) {
becameBusyAt = SystemClock.uptimeMillis();
}
if (debugCounting) {
Log.i(TAG, "Resource: " + resourceName + " in-use-count incremented to: " + (counterVal + 1));
}
}
/**
* Decrements the count of in-flight transactions to the resource being monitored.
*
* If this operation results in the counter falling below 0 - an exception is raised.
*
* @throws IllegalStateException if the counter is below 0.
*/
public void decrement() {
int counterVal = counter.decrementAndGet();
if (counterVal == 0) {
// we've gone from non-zero to zero. That means we're idle now! Tell espresso.
if (null != resourceCallback) {
resourceCallback.onTransitionToIdle();
}
becameIdleAt = SystemClock.uptimeMillis();
}
if (debugCounting) {
if (counterVal == 0) {
Log.i(TAG, "Resource: " + resourceName + " went idle! (Time spent not idle: " +
(becameIdleAt - becameBusyAt) + ")");
} else {
Log.i(TAG, "Resource: " + resourceName + " in-use-count decremented to: " + counterVal);
}
}
checkState(counterVal > -1, "Counter has been corrupted!");
}
/**
* Prints the current state of this resource to the logcat at info level.
*/
public void dumpStateToLogs() {
StringBuilder message = new StringBuilder("Resource: ")
.append(resourceName)
.append(" inflight transaction count: ")
.append(counter.get());
if (0 == becameBusyAt) {
Log.i(TAG, message.append(" and has never been busy!").toString());
} else {
message.append(" and was last busy at: ")
.append(becameBusyAt);
if (0 == becameIdleAt) {
Log.w(TAG, message.append(" AND NEVER WENT IDLE!").toString());
} else {
message.append(" and last went idle at: ")
.append(becameIdleAt);
Log.i(TAG, message.toString());
}
}
}
}
在耗時線程中調(diào)用
//耗時操作開始調(diào)用
helloWorldServerIdlingResource.increment();
{
//做一些耗時操作
}
//結(jié)束后調(diào)用
helloWorldServerIdlingResource.decrement();
</br>
UI Automator
UI Automator 測試框架提供了一組 API 來構(gòu)建 UI 測試,用于在用戶應(yīng)用和系統(tǒng)應(yīng)用中執(zhí)行交互。利用 UI Automator API,您可以執(zhí)行在測試設(shè)備中打開“設(shè)置”菜單或應(yīng)用啟動器等操作。UI Automator 測試框架非常適合編寫黑盒自動化測試,其中的測試代碼不依賴于目標(biāo)應(yīng)用的內(nèi)部實現(xiàn)詳情。
UI Automator 測試框架的主要功能包括:
- 用于檢查布局層次結(jié)構(gòu)的查看器。
- 在目標(biāo)設(shè)備上檢索狀態(tài)信息并執(zhí)行操作的 API。
- 支持跨應(yīng)用 UI 測試的 API。
要求 Android 4.3(API 級別 18)或更高版本。
UI Automator 查看器
uiautomatorviewer 工具提供了一個方便的 GUI,可以掃描和分析 Android 設(shè)備上當(dāng)前顯示的 UI 組件。您可以使用此工具檢查布局層次結(jié)構(gòu),并查看在設(shè)備前臺顯示的 UI 組件屬性。利用此信息,您可以使用 UI Automator(例如,通過創(chuàng)建與特定可見屬性匹配的 UI 選擇器)創(chuàng)建控制更加精確的測試。
uiautomatorviewer 工具位于 <android-sdk>/tools/ 目錄中。
UI Automator API
-
UiDevice
:用于在目標(biāo)應(yīng)用運行的設(shè)備上訪問和執(zhí)行操作。您可以調(diào)用其方法來訪問設(shè)備屬性,如當(dāng)前屏幕方向或顯示尺寸,按“返回”、“主屏幕”或“菜單”按鈕等。 -
UiCollection
:枚舉容器的 UI 元素以便計算子元素個數(shù),或者通過可見的文本或內(nèi)容描述屬性來指代子元素。 -
UiObject
:表示設(shè)備上可見的 UI 元素。 -
UiScrollable
:為在可滾動 UI 容器中搜索項目提供支持。 -
UiSelector
:表示在設(shè)備上查詢一個或多個目標(biāo) UI 元素。 -
Configurator
:允許您設(shè)置運行 UI Automator 測試所需的關(guān)鍵參數(shù)。
// 初始化 UiDevice
mDevice = UiDevice.getInstance(getInstrumentation());
// 按下home鍵
mDevice.pressHome();
//在當(dāng)前主界面,查找一個叫test的元素
UiObject allAppsButton = mDevice.findObject(new UiSelector().description("test"));
// 找到后點擊它
allAppsButton.click();
更多詳細(xì)的使用會在后面實際使用中講到。
壓力測試 Monkey
Monkey是Android中的一個命令行工具,可以運行在模擬器里或?qū)嶋H設(shè)備中。它向系統(tǒng)發(fā)送偽隨機的用戶事件流(如按鍵輸入、觸摸屏輸入、手勢輸入等),實現(xiàn)對正在開發(fā)的應(yīng)用程序進(jìn)行壓力測試。Monkey測試是一種為了測試軟件的穩(wěn)定性、健壯性的快速有效的方法。
Monkey的特征
- 測試的對象僅為應(yīng)用程序包,有一定的局限性。
- Monky測試使用的事件流數(shù)據(jù)流是隨機的,不能進(jìn)行自定義。
- 可對MonkeyTest的對象,事件數(shù)量,類型,頻率等進(jìn)行設(shè)置。
Monkey使用
adb shell monkey [options] <event-count>
options這個是配置monkey的設(shè)置,例如指定啟動那個包,不指定將會隨機啟動所有程序。event-count這個是讓monkey發(fā)送多少次事件。
adb shell monkey -p com.android.test -v 5000
這就是一個簡單的測試,向com.android.test包對應(yīng)的程序發(fā)送5000次隨機的事件,-p指定了測試的包名,-v指定了發(fā)送的隨機事件次數(shù)。

Monkey停止條件
- 如果限定了Monkey運行在一個或幾個特定的包上,那么它會監(jiān)測試圖轉(zhuǎn)到其它包的操作,并對其進(jìn)行阻止。
- 如果應(yīng)用程序崩潰或接收到任何失控異常,Monkey將停止并報錯。
- 如果應(yīng)用程序產(chǎn)生了應(yīng)用程序不響應(yīng)(ANR)的錯誤,Monkey將會停止并報錯。