Espresso 是一個 Google 官方提供的 Android 應用 UI 自動化測試架構。Google 希望,當Android 的開發者利用 Espresso 寫完測試用例後,能一邊看着測試用例自動執行,一邊享受一杯香醇 Espresso(濃咖啡)。
歡迎大家前往騰訊雲社群,擷取更多騰訊海量技術實踐幹貨哦~
Espresso是一個Google官方提供的Android應用UI自動化測試架構。Google希望,當Android的開發者利用Espresso寫完測試用例後,能一邊看着測試用例自動執行,一邊享受一杯香醇Espresso(濃咖啡)。
Espress有3個特點:
- 第一個收錄在Android Testing Supporting Library底下的測試架構
- 模拟使用者的操作
- 自動等待,直到UI線程Idle,才會執行測試代碼
接下來,将從配置、寫用例、運作一步步介紹Espresso的使用。
0. 項目配置
0.1 修改App的 build.gradle
build.gradle
- 在defaultConfig内增加,testInstrumentationRunner “android.support.test.runner.AndroidJUnitRunner”,用來運作腳本
- 增加packagingOptions,避免編譯時候License的沖突
-
在dependencies中增加相關的引用(androidTestCompile隻有在編譯測試用例時候才會運作,普通編譯不會)
下面是
中涉及到Espresso配置的内容build.gradle
android {
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
packagingOptions {
exclude 'LICENSE.txt'
}
}
dependencies {
// Espresso 相關的引用
compile 'com.android.support:support-annotations:22.1.1'
androidTestCompile 'com.android.support:support-annotations:22.1.1'
androidTestCompile('com.android.support.test.espresso:espresso-core:2.1'){
exclude group: 'javax.inject'
}
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.1'
androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.1'
androidTestCompile 'com.android.support.test:runner:0.2'
}
0.2 添加TestRunner
點選頂欄菜單Run->Edit Configurations,出現如下的視窗後,點選左上角的”+”,選擇”Android Tests”;
修改新Configuration的名字,選中App Module,輸入Runner,選擇”Show chooer dialog”,點選”OK”完成
1. 寫測試用例
1.1 三步曲
寫UI自動化測試用例,歸結起來就是3步:
定位View控件
操作View控件
校驗View控件的狀态
對應Espresso,就是以下3個方法的調用:
onView(ViewMatcher)
.perform(ViewAction)
.check(ViewAssertion);
其中,onView是用來定位View控件的,perform是操作控件的,check是校驗View控件的狀态。他們各自都需要再傳入對應的參數分别如下:
ViewMatcher,有withId、withText、withClassName等等方法來定位View控件
ViewAction,有click()、longClick()、pressBack()、swipeLeft()等等方法來操作View控件
ViewAssertion,有isEnabled()、isLeftOf()、isChecked()等等方法來校驗View控件狀态
這裡有
ViewMatcher
、
ViewAction
ViewAssertion
的Cheat Sheet。
1.2 完整測試用例代碼
這是一個非常簡單的測試用例,通過R.id.button定位控件,對它調用了一下click,最後校驗控件是不是enabled狀态。這裡面有一些注解,
@Rule
修飾的是被測試的
Activity
,
@Test
修飾的方法是測試用例。
@RunWith(AndroidJUnit4.class)public class MainActivityTest { @Rule
public ActivityTestRule mActivityRule = new ActivityTestRule(MainActivity.class); @Test
public void testTextViewDisplay() {
onView(withId(R.id.button))
.perform(click())
.check(matches(isEnabled()));
}
}
1.3 注意
Getting Started With Espresso 2.0這個視訊中提到了2個寫測試用例時的注意項:
避免
Activity
的層級跳轉,測試用例盡量隻在單個
Activity
内完成。
Activity
層級跳轉越多,越容易出錯
強烈不推薦,直接擷取
View
的對象,調用
View
的方法來模拟使用者操作。應該統一使用Espresso提供的方法
測試用例,特别是UI自動化測試用例,應該盡量保持邏輯簡單,覆寫關鍵路徑就足矣。因為UI變動是很頻繁的,越複雜,維護成本就越高,投入産出比就會自然降低了。
2. 運作用例
- 在運作菜單中選擇步驟0.2中設定的
,點選執行TestRunner
- 測試用例模拟使用者操作自動運作
- 測試用例執行完成,在Android Studio的控制台上,能看到如下的結果輸出
其中,看到”Done 3 of 3”辨別,一共3個檢查點,都檢查通過了。如果有檢查不通過的話,右上角的綠色能量條會變成紅色。
3. 進階
3.1 onData的使用
對于
ListView
,如果要操作其中的某一個item,特别是不可見狀态的item,是不能通過上述的
ViewMatch
來定位的。我們都知道
ListView
的
View
是複用的,不可見狀态的item并沒有把内容繪制到
View
上。Espresso針對
AdapterView
(
ListView
的父類),提供了
onData
來支援。
onData(ObjectMatcher)
.DataOptions
.perform(ViewAction)
.check(ViewAssertion);
onData
傳入的是一個
ObjectMather
。首先假設ListView的Adapter中的Item的定義如下:
public static class Item {
private final int value;
public Item(int value) {
this.value = value;
}
public String toString() {
return String.valueOf(value);
}
}
下面定義一個
withValue()
的方法,傳回一個
BoundedMatcher
。而其中的
matchesSafely()
方法是用來判斷match與否的,判斷的邏輯實作都放在這裡。
public static Matcher<Object> withValue(final int value) {
return new BoundedMatcher<Object,
MainActivity.Item>(MainActivity.Item.class) {
@Override public void describeTo(Description description) {
description.appendText("has value " + value);
}
@Override public boolean matchesSafely(
MainActivity.Item item) {
return item.toString().equals(String.valueOf(value));
}
};
}
有了上面的鋪墊,測試用例寫起來就水到渠成了。在id是
R.id.list
AdapterView
中找到資料項是
27
,然後執行
click()
操作。
@Test
public void clickItem() {
onData(withValue(27))
.inAdapterView(withId(R.id.list))
.perform(click());
//Do the assertion here.
}
最後需要注意的是,
onData()
并不适用于
RecyclerView
,因為它不是繼承自
AdapterView
。Espresso提供專門給
RecyclerView
使用的
RecyclerViewActions
。
@Test
public void clickItem() {
onView(withId(R.id.recycler_view))
.perform(
RecyclerViewActions.actionOnItemAtPosition(27, click()));
}
3.2 Idling Resource的使用
應用開發中很常見的一個場景是,點選某個按鈕,發起網絡請求,等請求回來後解析資料,更新界面。Espresso針對這種測試場景,提供了原生的支援。
假設被測Activity初始化後有一個耗時的資料加載過程,activity.isSyncFinished()方法判斷資料加載是否已經完成。代碼如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
//模拟耗時的資料加載
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
mIsSyncFinished = true;
}
}, 5000);
}
private volatile boolean mIsSyncFinished = false;
public boolean isSyncFinished() {
return mIsSyncFinished;
}
這種情況,Espresso提供了
IdlingResource
來保證資料加載完成了才開始執行測試用例代碼。首先,需實作
IdlingResource
接口:
- getName()方法傳回的String是作為注冊回調的Key,是以要確定唯一性
- registerIdleTransitionCallback()的參數ResourceCallback會用做isIdleNow()時候的回調
- isIdleNow()是否已經處于空閑狀态,這裡調用activity.isSyncFinished()方法來判斷資料加載是否完成
private static class MyIdlingResource implements IdlingResource {
private ResourceCallback mCallback = null;
private MainActivity mActivity;
MyIdlingResource(MainActivity activity) {
mActivity = activity;
}
@Override
public String getName() {
return "MyIdlingResource";
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
mCallback = callback;
}
@Override
public boolean isIdleNow() {
boolean isIdle = mActivity != null && mActivity.isSyncFinished();
if (isIdle && mCallback != null) {
mCallback.onTransitionToIdle();
}
return isIdle;
}
}
MyIdlingResource
需要在恰當的時機注冊和反注冊。
@Before
和
@After
是依照JUnit4的慣例,分别在用例執行之前和之後去注冊和反注冊。那麼,如下測試用例執行的過程是:
- 測試用例啟動,注冊MyIdlingResource
- 啟動被測Activity
- Activity初始化,啟動資料加載過程
- Activity資料加載完成,執行測試用例方法testTextViewDisplay()
-
測試用例結束,反注冊MyIdlingResource
可見,IdlingResource能夠保證流轉到Idle狀态,才會執行測試代碼:
@Test
public void testTextViewDisplay() {
onView(withText("Show SnackBar")).check(ViewAssertions.matches(isDisplayed()));
}
@Before
public void registerIntentServiceIdlingResource() {
Activity activity = mActivityRule.getActivity();
idlingResource = new MyIdlingResource((MainActivity) activity);
Espresso.registerIdlingResources(idlingResource);
}
@After
public void unregisterIntentServiceIdlingResource() {
Espresso.unregisterIdlingResources(idlingResource);
}
3.3. 執行原理
本文開頭提到Espresso其中一個特點,無需主動寫Sleep等待UI事件的執行和UI的繪制。原因是,Espresso的用例運作過程是隻有當UI線程IDLE和UI隊列沒有需要執行的事件時,Espresso的測試代碼才會被執行。使用方無需寫Sleep邏輯等待UI繪制完成。以下是Espresso測試用例執行簡易的流程圖,幫助了解:
寫在最後
引用官方介紹的一段話,Espresso的目标閱聽人是開發者。希望更多的團隊能夠實作Google的期許最大化利用Espresso,把Bug扼殺在搖籃中。
Target Audience
Espresso is targeted at developers, who believe that automated testing is an integral part of the development lifecycle. While it can be used for black-box testing, Espresso’s full power is unlocked by those who are familiar with the codebase under test.
引用
Getting Started With Espresso 2.0:https://www.youtube.com/watch?v=TGU0B4qRlHY
Advanced Android Espresso:https://realm.io/news/chiu-ki-chan-advanced-android-espresso-testing/
Android Espresso 測試架構探究:http://blog.csdn.net/weijianfeng1990912/article/details/51540468
Android自動化測試-AdapterView的測試:https://segmentfault.com/a/1190000004392396
Android單元測試研究與實踐:http://tech.meituan.com/Android_unit_test.html
文章來自: QQ音樂技術團隊 公衆号
相關閱讀
網頁加速特技之 AMP
表格行與列邊框樣式處理的原理分析及實戰應用
「騰訊雲遊戲開發者技術沙龍」11月24 日深圳站報名開啟 暢談遊戲加速
此文已由作者授權騰訊雲技術社群釋出,轉載請注明原文出處
原文連結:https://cloud.tencent.com/community/article/520289?utm_source=bky
海量技術實踐經驗,盡在騰訊雲社群!
海量技術實踐經驗,盡在雲加社群!
https://cloud.tencent.com/developer
build.gradle
build.gradle
-
build.gradle
android {
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
packagingOptions {
exclude 'LICENSE.txt'
}
}
dependencies {
// Espresso 相關的引用
compile 'com.android.support:support-annotations:22.1.1'
androidTestCompile 'com.android.support:support-annotations:22.1.1'
androidTestCompile('com.android.support.test.espresso:espresso-core:2.1'){
exclude group: 'javax.inject'
}
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.1'
androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.1'
androidTestCompile 'com.android.support.test:runner:0.2'
}
onView(ViewMatcher)
.perform(ViewAction)
.check(ViewAssertion);
ViewMatcher
ViewAction
ViewAssertion
@Rule
Activity
@Test
@RunWith(AndroidJUnit4.class)public class MainActivityTest { @Rule
public ActivityTestRule mActivityRule = new ActivityTestRule(MainActivity.class); @Test
public void testTextViewDisplay() {
onView(withId(R.id.button))
.perform(click())
.check(matches(isEnabled()));
}
}
Activity
Activity
Activity
View
View
-
TestRunner
ListView
ViewMatch
ListView
View
View
AdapterView
ListView
onData
onData(ObjectMatcher)
.DataOptions
.perform(ViewAction)
.check(ViewAssertion);
onData
ObjectMather
public static class Item {
private final int value;
public Item(int value) {
this.value = value;
}
public String toString() {
return String.valueOf(value);
}
}
withValue()
BoundedMatcher
matchesSafely()
public static Matcher<Object> withValue(final int value) {
return new BoundedMatcher<Object,
MainActivity.Item>(MainActivity.Item.class) {
@Override public void describeTo(Description description) {
description.appendText("has value " + value);
}
@Override public boolean matchesSafely(
MainActivity.Item item) {
return item.toString().equals(String.valueOf(value));
}
};
}
R.id.list
AdapterView
27
click()
@Test
public void clickItem() {
onData(withValue(27))
.inAdapterView(withId(R.id.list))
.perform(click());
//Do the assertion here.
}
onData()
RecyclerView
AdapterView
RecyclerView
RecyclerViewActions
@Test
public void clickItem() {
onView(withId(R.id.recycler_view))
.perform(
RecyclerViewActions.actionOnItemAtPosition(27, click()));
}
@Override
protected void onCreate(Bundle savedInstanceState) {
//模拟耗時的資料加載
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
mIsSyncFinished = true;
}
}, 5000);
}
private volatile boolean mIsSyncFinished = false;
public boolean isSyncFinished() {
return mIsSyncFinished;
}
IdlingResource
IdlingResource
private static class MyIdlingResource implements IdlingResource {
private ResourceCallback mCallback = null;
private MainActivity mActivity;
MyIdlingResource(MainActivity activity) {
mActivity = activity;
}
@Override
public String getName() {
return "MyIdlingResource";
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
mCallback = callback;
}
@Override
public boolean isIdleNow() {
boolean isIdle = mActivity != null && mActivity.isSyncFinished();
if (isIdle && mCallback != null) {
mCallback.onTransitionToIdle();
}
return isIdle;
}
}
MyIdlingResource
@Before
@After
@Test
public void testTextViewDisplay() {
onView(withText("Show SnackBar")).check(ViewAssertions.matches(isDisplayed()));
}
@Before
public void registerIntentServiceIdlingResource() {
Activity activity = mActivityRule.getActivity();
idlingResource = new MyIdlingResource((MainActivity) activity);
Espresso.registerIdlingResources(idlingResource);
}
@After
public void unregisterIntentServiceIdlingResource() {
Espresso.unregisterIdlingResources(idlingResource);
}