前言
想想接觸Android也有三年多的時(shí)間了证薇,實(shí)際開(kāi)發(fā)也有兩年的時(shí)間了,好像也很少接觸到Android自動(dòng)化測(cè)試匆篓,雖然偶有聽(tīng)說(shuō)浑度,但也沒(méi)有認(rèn)真的學(xué)習(xí)過(guò)。相信很多朋友跟我也有一樣的經(jīng)歷鸦概,對(duì)自動(dòng)化測(cè)試不了解箩张,加上項(xiàng)目沒(méi)有要求,認(rèn)為自動(dòng)化測(cè)試價(jià)值不高窗市,完全是浪費(fèi)時(shí)間先慷。但實(shí)際的情況并不是這樣,前段時(shí)間聽(tīng)一個(gè)朋友講了些Android自動(dòng)化測(cè)試咨察,給了我很深的印象熟掂。正因?yàn)檫@樣的契機(jī),所以前段時(shí)間也花時(shí)間學(xué)了Android基本的自動(dòng)化測(cè)試扎拣。趁最近剛好有空整理了一下自己的學(xué)習(xí)心得。
大家可以看一下這篇文章素跺,可能會(huì)說(shuō)服你:為什么要進(jìn)行煩人的單元測(cè)試二蓝?
Android Testing Support Library
在2015年Google I/O大會(huì)上,Google放出了一個(gè)Android Testing Support Library指厌,該庫(kù)提供了大量用于測(cè)試 Android 應(yīng)用的框架刊愚。此庫(kù)提供了一組 API,讓您可以為應(yīng)用快速構(gòu)建何運(yùn)行測(cè)試代碼踩验,包括單元測(cè)試 JUnit 4 和功能性用戶(hù)界面 (UI) 測(cè)試鸥诽。我們可以從Android Studio IDE或命令行運(yùn)行使用這些 API 創(chuàng)建的測(cè)試。 在測(cè)試庫(kù)中包含AndroidJUnitRunner類(lèi)是一個(gè)JUnit測(cè)試運(yùn)行器箕憾,可讓我們?cè)?Android 設(shè)備上運(yùn)行 JUnit 3 或 JUnit 4 樣式測(cè)試類(lèi)牡借,包括使用Espresso和UI Automator測(cè)試框架的設(shè)備。測(cè)試運(yùn)行器可以將測(cè)試軟件包和要測(cè)試的應(yīng)用加載到設(shè)備袭异、運(yùn)行測(cè)試并報(bào)告測(cè)試結(jié)果钠龙。所以后面會(huì)講到的單元測(cè)試和UI測(cè)試的詳細(xì)使用,都是基于Android Testing Support Library。
單元測(cè)試 JUnit 4
我們?cè)趯?shí)際項(xiàng)目開(kāi)發(fā)中的時(shí)候碴里,都是需要寫(xiě)成千上萬(wàn)個(gè)方法或函數(shù)沈矿,這些函數(shù)的功能可能很強(qiáng)大,也可能是很小一個(gè)功能咬腋,但我們?cè)诔绦蛑惺褂脮r(shí)都是需要經(jīng)過(guò)測(cè)試的羹膳,保證這一部分功能是正確的。所以說(shuō)根竿,每編寫(xiě)完一個(gè)函數(shù)之后陵像,都應(yīng)該對(duì)這個(gè)函數(shù)的方方面面進(jìn)行測(cè)試,這樣的測(cè)試我們稱(chēng)之為單元測(cè)試犀填。傳統(tǒng)的編程方式蠢壹,進(jìn)行單元測(cè)試是一件很麻煩的事情,我們需要在該程序中調(diào)用你需要測(cè)試的方法九巡,并且仔細(xì)觀(guān)察運(yùn)行結(jié)果图贸,看看是否有錯(cuò)。正因?yàn)槿绱寺闊┟峁悖跃陀辛撕芏鄦卧獪y(cè)試框架疏日,JUnit 4就是其中一種。
本地單元測(cè)試 Local Unit Tests
這種測(cè)試運(yùn)行在本地開(kāi)發(fā)環(huán)境的Java虛擬機(jī)上撒汉,也不需要連接Android設(shè)備或者模擬器沟优,因此并無(wú)法獲得Android相關(guān)的API,所以只能測(cè)試只使用Java API的一些功能睬辐。
測(cè)試類(lèi)代碼編寫(xiě)也很簡(jiǎn)單挠阁,主要通過(guò)一些注解來(lái)標(biāo)示,同時(shí)可以通過(guò)assertXXXX來(lái)斷言結(jié)果
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 4);
}
}
Junit 4注解
- @Before標(biāo)注setup方法溯饵,每個(gè)單元測(cè)試用例方法調(diào)用之前都會(huì)調(diào)用
- @After標(biāo)注teardown方法侵俗,每個(gè)單元測(cè)試用例方法調(diào)用之后都會(huì)調(diào)用
- @Test標(biāo)注的每個(gè)方法都是一個(gè)測(cè)試用例
- @BeforeClass標(biāo)注的靜態(tài)方法,在當(dāng)前測(cè)試類(lèi)所有用例方法執(zhí)行之前執(zhí)行
- @AfterClass標(biāo)注的靜態(tài)方法丰刊,在當(dāng)前測(cè)試類(lèi)所有用例方法執(zhí)行之后執(zhí)行
- @Test(timeout=)為測(cè)試用例指定超時(shí)時(shí)間
斷言
Junit提供了一系列斷言來(lái)判斷是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è)備單元測(cè)試 Instrumented Unit Tests
這種測(cè)試方式需要連接Android設(shè)備或模擬器≈绕停可以利用Android框架API码泛,比如測(cè)試需要訪(fǎng)問(wèn)設(shè)備信息(如目標(biāo)應(yīng)用程序的上下文中)或如果他們需要一個(gè)Android 相關(guān)的API(如Parcelable或SharedPreferences對(duì)象)。在使用上也很簡(jiǎn)單澄耍,相比本地單元測(cè)試該測(cè)試類(lèi)必須以 @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 多了訪(fǎng)問(wèn)設(shè)備信息忘衍、測(cè)試篩選
訪(fǎng)問(wèn)設(shè)備信息
我們可以使用 InstrumentationRegistry
類(lèi)訪(fǎng)問(wèn)與測(cè)試運(yùn)行相關(guān)的信息。此類(lèi)包括 Instrumentation對(duì)象卿城、目標(biāo)應(yīng)用Context對(duì)象枚钓、測(cè)試應(yīng)用Context對(duì)象,以及傳遞到測(cè)試中的命令行參數(shù)瑟押。
測(cè)試篩選
- @RequiresDevice:指定測(cè)試僅在物理設(shè)備而不在模擬器上運(yùn)行搀捷。
- @SdkSupress:禁止在低于給定級(jí)別的 Android API 級(jí)別上運(yùn)行測(cè)試。例如多望,要禁止在低于 18 的所有 API 級(jí)別上運(yùn)行測(cè)試嫩舟,請(qǐng)使用注解 @SDKSupress(minSdkVersion=18)。
- @SmallTest怀偷、@MediumTest和@LargeTest:指定測(cè)試的運(yùn)行時(shí)長(zhǎng)以及運(yùn)行頻率家厌。
UI測(cè)試
Espresso
Espresso 測(cè)試框架提供了一組 API 來(lái)構(gòu)建 UI 測(cè)試,用于測(cè)試應(yīng)用中的用戶(hù)流椎工。利用這些 API饭于,您可以編寫(xiě)簡(jiǎn)潔、運(yùn)行可靠的自動(dòng)化 UI 測(cè)試维蒙。Espresso 非常適合編寫(xiě)白盒自動(dòng)化測(cè)試掰吕,其中測(cè)試代碼將利用所測(cè)試應(yīng)用的實(shí)現(xiàn)代碼詳情。
Espresso 測(cè)試框架的主要功能包括:
- 靈活的 API颅痊,用于目標(biāo)應(yīng)用中的視圖和適配器匹配殖熟。
- 一組豐富的操作 API,用于自動(dòng)化 UI 交互斑响。
- UI 線(xiàn)程同步菱属,用于提升測(cè)試可靠性。
要求 Android 2.2(API 級(jí)別 8)或更高版本舰罚。
視圖匹配
利用Espresso.onView()
方法纽门,您可以訪(fǎng)問(wèn)目標(biāo)應(yīng)用中的 UI 組件并與之交互。此方法接受Matcher
參數(shù)并搜索視圖層次結(jié)構(gòu)沸停,以找到符合給定條件的相應(yīng)View
實(shí)例。您可以通過(guò)指定以下條件來(lái)優(yōu)化搜索:
- 視圖的類(lèi)名稱(chēng) onView(withClassName());
- 視圖的內(nèi)容描述 onView(withContentDescription());
- 視圖的ID onView(withId());
- 在視圖中顯示的文本 onView(withText());
更多的可以查看ViewMatchers昭卓。如果搜索成功愤钾,onView()
方法將返回一個(gè)引用,讓您可以執(zhí)行用戶(hù)操作并基于目標(biāo)視圖對(duì)斷言進(jìn)行測(cè)試候醒。
適配器匹配
在AdapterView
布局中能颁,布局在運(yùn)行時(shí)由子視圖動(dòng)態(tài)填充。如果目標(biāo)視圖位于某個(gè)布局內(nèi)部倒淫,而該布局是從AdapterView
(例如ListView
或GridView
)派生出的子類(lèi)伙菊,則onView()
方法可能無(wú)法工作,因?yàn)橹挥胁季忠晥D的子集會(huì)加載到當(dāng)前視圖層次結(jié)構(gòu)中。因此镜硕,需要使用Espresso.onData()
方法訪(fǎng)問(wèn)目標(biāo)視圖元素运翼。Espresso.onData()
方法將返回一個(gè)引用,讓您可以執(zhí)行用戶(hù)操作并根據(jù)AdapterView
中的元素對(duì)斷言進(jìn)行測(cè)試兴枯。
//點(diǎn)擊spinner
onView(withId(R.id.spinner)).perform(click());
//點(diǎn)擊adpaterviewer中類(lèi)型為String 并且內(nèi)容為test的文本血淌,
onData(allOf(is(instanceOf(String.class)),is("test"))).perform(click());
操作API
在上面的一段代碼中,我們用到了perform(click())财剖,那么除了click()方法還有其他功能強(qiáng)大的方法可以供我們使用悠夯,下面列舉一些常用的方法:
- click():返回一個(gè)點(diǎn)擊動(dòng)作,Espresso利用這個(gè)方法執(zhí)行一次點(diǎn)擊操作,就和我們自己手動(dòng)點(diǎn)擊按鈕一樣躺坟。
- clearText():返回一個(gè)清除指定view中的文本action沦补,在測(cè)試EditText時(shí)用的比較多。
- swipeLeft():返回一個(gè)從右往左滑動(dòng)的action,這個(gè)在測(cè)試ViewPager時(shí)特別有用咪橙。
- swipeRight():返回一個(gè)從左往右滑動(dòng)的action,這個(gè)在測(cè)試ViewPager時(shí)特別有用夕膀。
- swipeDown():返回一個(gè)從上往下滑動(dòng)的action。
- swipeUp():返回一個(gè)從下往上滑動(dòng)的action匣摘。
- closeSoftKeyboard():返回一個(gè)關(guān)閉輸入鍵盤(pán)的action店诗。
- pressBack():返回一個(gè)點(diǎn)擊手機(jī)上返回鍵的action。
- doubleClick():返回一個(gè)雙擊action
- longClick():返回一個(gè)長(zhǎng)按action
更多的可以查看ViewActions音榜。
校驗(yàn)結(jié)果
調(diào)用ViewInteraction.check()
和DataInteraction.check()
方法庞瘸,可以判斷UI元素的狀態(tài),如果斷言失敗赠叼,會(huì)拋出AssertionFailedError異常擦囊。
- doesNotExist:斷言某一個(gè)view不存在。
- matches:斷言某個(gè)view存在嘴办,且符合一列的匹配瞬场。
- selectedDescendentsMatch:斷言指定的子元素存在,且他們的狀態(tài)符合一些列的匹配涧郊。
onView(withId(R.id.textview)).check(matches(withText("test")));
UI 線(xiàn)程同步
Espresso 的核心是它可以與待測(cè)應(yīng)用無(wú)縫同步測(cè)試操作的能力贯被。默認(rèn)情況下,Espresso 會(huì)等待當(dāng)前消息隊(duì)列中的 UI 事件執(zhí)行(默認(rèn)是 AsyncTask)完畢再進(jìn)行下一個(gè)測(cè)試操作妆艘。這應(yīng)該能解決大部分應(yīng)用與測(cè)試同步的問(wèn)題彤灶。然而,應(yīng)用中有一些執(zhí)行后臺(tái)操作的對(duì)象(比如與網(wǎng)絡(luò)服務(wù)交互)通過(guò)非標(biāo)準(zhǔn)方式實(shí)現(xiàn)批旺;例如:直接創(chuàng)建和管理線(xiàn)程幌陕,以及使用自定義服務(wù)。慶幸的是 Espresso 仍然可以同步測(cè)試操作與你的自定義資源汽煮。
以下是我們需要完成的:
- 實(shí)現(xiàn) ?IdlingResource 接口并暴露給測(cè)試搏熄。
- 通過(guò)調(diào)用ResourceCallback..onTransitionToIdle()通知Espresso棚唆。
需要注意的是 IdlingResource 接口是在待測(cè)應(yīng)用中實(shí)現(xiàn)的,所以你需要添加依賴(lài):
compile 'com.android.support.test.espresso:espresso-idling-resource:2.2.2'
下面我們看看官方的例子是如何實(shí)現(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());
}
}
}
}
在耗時(shí)線(xiàn)程中調(diào)用
//耗時(shí)操作開(kāi)始調(diào)用
helloWorldServerIdlingResource.increment();
{
//做一些耗時(shí)操作
}
//結(jié)束后調(diào)用
helloWorldServerIdlingResource.decrement();
</br>
UI Automator
UI Automator 測(cè)試框架提供了一組 API 來(lái)構(gòu)建 UI 測(cè)試心例,用于在用戶(hù)應(yīng)用和系統(tǒng)應(yīng)用中執(zhí)行交互宵凌。利用 UI Automator API,您可以執(zhí)行在測(cè)試設(shè)備中打開(kāi)“設(shè)置”菜單或應(yīng)用啟動(dòng)器等操作契邀。UI Automator 測(cè)試框架非常適合編寫(xiě)黑盒自動(dòng)化測(cè)試摆寄,其中的測(cè)試代碼不依賴(lài)于目標(biāo)應(yīng)用的內(nèi)部實(shí)現(xiàn)詳情。
UI Automator 測(cè)試框架的主要功能包括:
- 用于檢查布局層次結(jié)構(gòu)的查看器坯门。
- 在目標(biāo)設(shè)備上檢索狀態(tài)信息并執(zhí)行操作的 API微饥。
- 支持跨應(yīng)用 UI 測(cè)試的 API。
要求 Android 4.3(API 級(jí)別 18)或更高版本古戴。
UI Automator 查看器
uiautomatorviewer
工具提供了一個(gè)方便的 GUI欠橘,可以?huà)呙韬头治?Android 設(shè)備上當(dāng)前顯示的 UI 組件。您可以使用此工具檢查布局層次結(jié)構(gòu)现恼,并查看在設(shè)備前臺(tái)顯示的 UI 組件屬性肃续。利用此信息,您可以使用 UI Automator(例如叉袍,通過(guò)創(chuàng)建與特定可見(jiàn)屬性匹配的 UI 選擇器)創(chuàng)建控制更加精確的測(cè)試始锚。
uiautomatorviewer 工具位于 <android-sdk>/tools/ 目錄中。
UI Automator API
-
UiDevice
:用于在目標(biāo)應(yīng)用運(yùn)行的設(shè)備上訪(fǎng)問(wèn)和執(zhí)行操作喳逛。您可以調(diào)用其方法來(lái)訪(fǎng)問(wèn)設(shè)備屬性瞧捌,如當(dāng)前屏幕方向或顯示尺寸,按“返回”润文、“主屏幕”或“菜單”按鈕等姐呐。 -
UiCollection
:枚舉容器的 UI 元素以便計(jì)算子元素個(gè)數(shù),或者通過(guò)可見(jiàn)的文本或內(nèi)容描述屬性來(lái)指代子元素典蝌。 -
UiObject
:表示設(shè)備上可見(jiàn)的 UI 元素曙砂。 -
UiScrollable
:為在可滾動(dòng) UI 容器中搜索項(xiàng)目提供支持。 -
UiSelector
:表示在設(shè)備上查詢(xún)一個(gè)或多個(gè)目標(biāo) UI 元素骏掀。 -
Configurator
:允許您設(shè)置運(yùn)行 UI Automator 測(cè)試所需的關(guān)鍵參數(shù)鸠澈。
// 初始化 UiDevice
mDevice = UiDevice.getInstance(getInstrumentation());
// 按下home鍵
mDevice.pressHome();
//在當(dāng)前主界面,查找一個(gè)叫test的元素
UiObject allAppsButton = mDevice.findObject(new UiSelector().description("test"));
// 找到后點(diǎn)擊它
allAppsButton.click();
更多詳細(xì)的使用會(huì)在后面實(shí)際使用中講到截驮。
壓力測(cè)試 Monkey
Monkey是Android中的一個(gè)命令行工具笑陈,可以運(yùn)行在模擬器里或?qū)嶋H設(shè)備中。它向系統(tǒng)發(fā)送偽隨機(jī)的用戶(hù)事件流(如按鍵輸入侧纯、觸摸屏輸入新锈、手勢(shì)輸入等)甲脏,實(shí)現(xiàn)對(duì)正在開(kāi)發(fā)的應(yīng)用程序進(jìn)行壓力測(cè)試眶熬。Monkey測(cè)試是一種為了測(cè)試軟件的穩(wěn)定性妹笆、健壯性的快速有效的方法。
Monkey的特征
- 測(cè)試的對(duì)象僅為應(yīng)用程序包娜氏,有一定的局限性拳缠。
- Monky測(cè)試使用的事件流數(shù)據(jù)流是隨機(jī)的,不能進(jìn)行自定義贸弥。
- 可對(duì)MonkeyTest的對(duì)象窟坐,事件數(shù)量,類(lèi)型绵疲,頻率等進(jìn)行設(shè)置哲鸳。
Monkey使用
adb shell monkey [options] <event-count>
options這個(gè)是配置monkey的設(shè)置,例如指定啟動(dòng)那個(gè)包盔憨,不指定將會(huì)隨機(jī)啟動(dòng)所有程序徙菠。event-count這個(gè)是讓monkey發(fā)送多少次事件。
adb shell monkey -p com.android.test -v 5000
這就是一個(gè)簡(jiǎn)單的測(cè)試郁岩,向com.android.test包對(duì)應(yīng)的程序發(fā)送5000次隨機(jī)的事件婿奔,-p指定了測(cè)試的包名,-v指定了發(fā)送的隨機(jī)事件次數(shù)问慎。
Monkey停止條件
- 如果限定了Monkey運(yùn)行在一個(gè)或幾個(gè)特定的包上萍摊,那么它會(huì)監(jiān)測(cè)試圖轉(zhuǎn)到其它包的操作,并對(duì)其進(jìn)行阻止如叼。
- 如果應(yīng)用程序崩潰或接收到任何失控異常冰木,Monkey將停止并報(bào)錯(cuò)。
- 如果應(yīng)用程序產(chǎn)生了應(yīng)用程序不響應(yīng)(ANR)的錯(cuò)誤薇正,Monkey將會(huì)停止并報(bào)錯(cuò)片酝。