友情提示:本篇文章內容較長,預計閱讀時間爲30分鐘。
前序
當我準備研究自動化測試這門技術時,其實我是懵逼的,我無法完全地分清功能測試、UI測試和自動化測試這三者之間的區別和聯繫,我也不知道自動化測試到底是測試什麼和不測試什麼?直到在網上看到這麼一個關於自動化測試的說法:
把自己當成用戶,只關注自己所能看到的東西。
嗯。。。感覺上面那個問題有答案了。
自動化測試這個東西其實就是讓機器去模仿人的行爲,讓它去做整個測試工作。既然是去模仿人的行爲,那實際上也應該認爲機器只能理解人所能理解的東西。比方說,當我去人爲地做一些測試的時候,我所期待的只是UI上的變化可以符合我的預期,至於它背後的數據是怎樣的實際上我並不關心。
這個思路的意思是在於,我要讓機器模擬我的測試過程,那麼我就需要針對那些我(作爲用戶)能看到的東西,也就是UI。比如說,我並不關心某個網絡請求返回值的具體數據是否正確,我關心的是我能在UI上看到我希望看到的結果。基於此,我覺得寫各個測試用例的一個通用的思路就是:
找到某個元素,做一些操作,檢查結果。
這裏包含了三個流程:
- 找元素:找到UI上測試所針對的元素;
- 做操作:給這個元素做一些操作;
- 檢查結果:這個元素做出了我期望的行爲。
再直觀一點,向一個表單輸入一段文字,那麼整個過程就可以描述爲:
- 找元素:找到EditText;
- 做操作:向EditText輸入字符串;
- 檢查結果:EditText顯示了我輸入的字符串。
以上三個步驟實際上是我們作爲用戶在使用一個APP的時候所遵循的流程,而測試也是基本遵循這樣一個流程的,各種自動化測試框架也是圍繞這三個步驟來提供支持,下面就來說說Espresso這款測試框架。
Espresso
一、簡介
Espresso 是 Google 官方提供的一個易於測試 Android UI 的開源框架,於2013年10月推出它的released 版本,目前最新版本已更新到2.x. 並且在AndroidStudio 2.2 預覽版中已經默認集成該測試庫.
Espresso 由以下三個基礎部分組成:
- ViewMatchers - 在當前View層級去匹配指定的View.
- ViewActions - 執行Views的某些行爲,如點擊事件.
- ViewAssertions - 檢查Views的某些狀態,如是否顯示.
使用Espresso框架的測試代碼大致如下:
onView(ViewMatcher) // 1.匹配View
.perform(ViewAction) // 2.執行View行爲
.check(ViewAssertion); // 3.驗證View
espresso提供瞭如下圖所示的API:
下面就將圍繞上圖中的這些API來進行講解。
二、準備
雖然android studio 2.2預覽版已經默認集成了espresso,但還是需要再做一些配置。
1. 首先需要在build.gradle的dependencies中增加如下依賴:
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: "com.android.support"
})
androidTestCompile('com.android.support.test:runner:0.5', {
exclude group: "com.android.support"
})
2. 其次需要更改testInstrumentationRunner
android {
...
defaultConfig {
...
testInstrumentationRunner" android.support.test.runner.AndroidJUnitRunner"
...
}
...
}
三、在哪裏寫
依賴配置好後,就可以開始編寫測試用例了!但問題是我們應該在哪裏添加測試用例呢?Espresso和其他自動化測試框架不同,在Android Studio中新建一個工程時,在src目錄下,和main平級的地方還有一個androidTest目錄,一般而言,我們將工程代碼放在src/main/java目錄下,將與之相關的測試代碼放在src/androidTest/java目錄下。如下所示:
src/
androidTest/java ----這裏存放instrumentation test相關的代碼
main/java ----這裏存放工程代碼
同時,爲了讓工程更容易維護,建議將相應Class的測試代碼放到相同名稱的包下面,比如,在Package-name下面有一個Class A:
src/main/java/package-name/A.java
那麼,建議將A的測試類放到androidTest下面對應的路徑下:
src/androidTest/java/package-name/ATest.java
四、編寫一個簡單的測試用例
假設有這樣一個登錄的場景:用戶輸入手機號和密碼後點擊登錄按鈕,登錄成功會toast提示"登錄成功",用戶名或者密碼錯誤導致登錄失敗會toast提示"用戶名或密碼錯誤"。
測試代碼爲:
@RunWith(AndroidJUnit4.class)
public class LoginActivityTest {
@Rule
public ActivityTestRule<LoginActivity> mRule = new ActivityTestRule<>(LoginActivity.class);
@Test
public void testLogin() {
// 獲取手機號的輸入框,輸入手機號,然後關閉軟鍵盤
onView(withId(R.id.et_mobile)).perform(typeText("17721429141"), closeSoftKeyboard());
// 獲取密碼的輸入框,輸入密碼,然後關閉軟鍵盤
onView(withId(R.id.et_password)).perform(typeText("123456"), closeSoftKeyboard());
// 點擊登錄按鈕
onView(withId(R.id.btn_login)).perform(click());
// 檢查是否有"登錄成功"的toast彈出
onView(withText("登錄成功")).inRoot(isToast()).check(matches(isDisplayed()));
}
}
代碼說明:
- @RunWith這個註解是必需的,定義測試代碼會在什麼樣的環境下運行,@RunWith(AndroidJUnit4.class)就表明是在android的環境下運行。
- @Rule這個註解是定義測試規則的,而ActivityTestRule是用來指定activity的啓動規則的,上面的代碼表明將LoginActivity作爲第一個activity來啓動。
- @Test這個註解是定義一個測試用例,當測試代碼運行時,所執行的代碼就是Test註解下的測試代碼。
相應的還有其他一些註解:
- @Before
- @After
- @BeforeClass
- @AfterClass
- @Test(timeout=)
五、查找元素
1.基礎查找方法
在上面那個登錄的測試用例中,都是用withId()方法來查找UI元素的,espresso也提供了很多查找Ui元素的方法,常用的有:
- withId()
- withText()
- withHint()
- withTagKey()
- withTagValue()
- hasLinks()
- hasFocus() ...
2.組合查找方法
當單一的查找條件不能唯一匹配某一個UI元素時,就可以通過組合多個查找條件來唯一確定某一個UI元素。
比如有這樣一個場景:
當需要點擊"小炒肉"旁邊的添加按鈕時,測試代碼能像下面這樣來寫:
onView(allOf(withText("添加"), hasSibling(withText("小炒肉")))).perform(click());
上面這行代碼的意思就是:點擊 有文本爲"小炒肉"的兄弟UI元素 且 自身的文本內容爲"添加"的UI元素。
3.自定義查找方法
上面的這些查找方法在平時的實際業務測試中基本上已經夠用了,但有時候或許需用通過更加複雜的條件去匹配某個UI元素,而espresso提供的方法又達不到想要的效果,這時自定義查找方法就排上用場了!
下面是一個自定義的匹配方法 - nthChildOf()的例子,它的作用是查找出某個UI元素的第幾個子元素:
public static Matcher<View> nthChildOf(final Matcher<View> parentMatcher, final int childPosition) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("position " + childPosition + " of parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
if (!(view.getParent() instanceof ViewGroup)) reurn false;
ViewGroup parent = (ViewGroup) view.gtParent();
return parentMatcher.matches(parent)
&& parent.getChildCount() > childPosition
&& parent.getChildAt(childPosition).equals(view);
}
};
}
六、操作元素
找到了目標元素,接下來我們該針對該元素做一些操作了。Espresso提供瞭如下方法來對相應的元素做操作:
public ViewInteraction perform(final ViewAction... viewActions) {}
該方法定義在ViewInteraction類裏面。還記得onView()方法的返回值麼?yes,正是一個ViewInteraction對象。因此,我們可以在onView()方法找到的元素上直接調用perform()方法進行一系列操作:
onView(withId(id)).perform(click())
如上代碼對onView()查詢到的元素做了一次點擊的操作。請注意,perform()方法的入參是變長參數,也就意味着,我們可以依次對某個元素做多個操作:
onView(withId(id)).perform(click(), replaceText(text), closeSoftKeyboard())
以上代碼對目標元素依次做了點擊、輸入文本、關閉輸入法鍵盤的操作。這是一個典型的填寫表單的行爲。
除了上面登錄的測試用例中使用的typeText()、closeSoftKeyboard()、click()外,espresso還提供瞭如下的行爲方法:
- doubleClick()
- longClick()
- pressBack()
- pressKey()
- openLink()
- scrollTo()
- swipeLeft()/swipeRight()/swipeUp()/swipeDown()
- clearText()
- replaceText()
七、檢查結果
到目前爲止,我們已經能找到元素,也能夠對元素進行一些操作了!接下來我們需要檢查一下這些操作的結果是否符合我們的預期。
Espresso提供了一個check()方法用來檢測結果:
public ViewInteraction check(final ViewAssertion viewAssert) {}
該方法接收了一個ViewAssertion的入參,該入參的作用就是檢查結果是否符合我們的預期。一般來說,我們可以調用如下的方法來自定義一個ViewAssertion:
public static ViewAssertion matches(final Matcher<? super View> viewMatcher) {}
這個方法接收了一個匹配規則,然後根據這個規則爲我們生成了一個ViewAssertion對象!還記得Matcher這個類型麼!!是的,這就是onView()方法的入參!實際上他們是同一個類型,其使用方法也是完全一致的。
比如,我想檢查一下指定id的TextView是否按照我的預期顯示了一段text文本,那麼我就可以這樣寫:
onView(withId(id)).check(matches(withText(text)))
進階
一、Idling Resource
Espresso官方文檔有這樣一段話:
Espresso測試有個很強大的地方是它在多個測試操作中是線程安全的。Espresso會等待當前進程的消息隊列中的UI事件,並且在任何一個測試操作中會等待其中的AsyncTask結束纔會執行下一個測試。這能夠解決程序中大部分的線程同步問題。
這句話的意思是Espresso在執行每一個測試操作時會檢查下面兩個場景:
- 在當前消息隊列中沒有UI事件;
- 在默認的AsyncTask線程池沒有任務;
當這兩種情況都滿足時纔會繼續執行測試操作。但是,如果App以其他方式執行長時間運行操作,Espresso不知道如何判斷這些操作已經完成。Espresso提供了IdlingResource這個API來達到延時操作的效果,IdlingResource本身是個接口,代碼如下:
public interface IdlingResource {
// 用於日誌顯示的名字,可隨意取
public String getName();
// 是否是空閒狀態
public boolean isIdleNow();
// 註冊變成空閒的回調
public void registerIdleTransitionCallback(ResourceCallback callback);
// 回調接口
public interface ResourceCallback {
public void onTransitionToIdle();
}
}
比如我們在登錄界面點擊登錄按鈕後,會在發起網絡請求前顯示一個LoadingDialog,LoadingDialog是用DialogFragment來實現的,如果我們想要在點擊了登錄按鈕、LoadingDialog消失後再檢查某條Toast是否彈出後,就可以通過IdlingResource來實現。
自定義IdlingResource的代碼如下:
public class LoadingDialogIdlingResource implements IdlingResource {
private FragmentManager manager;
private String tag;
private ResourceCallback callback;
public LoadingDialogIdlingResource(FragmentManager manager, String tag) {
this.manager = manager;
this.tag = tag;
}
@Override
public String getName() {
return LoadingDialogIdlingResource.class.getSimpleName();
}
@Override
public boolean isIdleNow() {
Fragment fragment = manager.findFragmentByTag(tag);
boolean idle = fragment == null || !(fragment instanceof DialogFragment) || !((DialogFragment) fragment).getDialog().isShowing();
if (idle && callback != null) {
callback.onTransitionToIdle();
}
return idle;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
this.callback = callback;
}
}
在測試用例中使用的代碼如下:
@RunWith(AndroidJUnit4.class)
public class LoginActivityTest {
@Rule
public ActivityTestRule<LoginActivity> mActivityRule = new ActivityTestRule<>(LoginActivity, true);
private LoadingDialogIdlingResource idlingResource;
@Before
public void setUp() {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
idlingResource = new LoadingDialogIdlingResource(mActivityRule.getActivity().getSupportFragmentManager(), "TAG");
Espresso.registerIdlingResources();
}
@After
public void tearDown() {
Espresso.unregisterIdlingResources(idlingResource);
idlingResource = null;
}
@Test
public void testLogin() {
// 獲取手機號的輸入框,輸入手機號,然後關閉軟鍵盤
onView(withId(R.id.et_mobile)).perform(typeText("17721429141"), closeSoftKeyboard());
// 獲取密碼的輸入框,輸入密碼,然後關閉軟鍵盤
onView(withId(R.id.et_password)).perform(typeText("123456"), closeSoftKeyboard());
// 點擊登錄按鈕
onView(withId(R.id.btn_login)).perform(click());
// 在這一步時會等待LoadingDialogIdlingResource的isIdleNow()返回true時纔會繼續往下執行
// 檢查是否有"登錄成功"的toast彈出
onView(withText("登錄成功")).inRoot(isToast()).check(matches(isDisplayed()));
}
}
二、Intent
在實際的測試業務中,當單獨測試某個界面時,或許會遇到這個界面需要它的上一個界面傳遞數據來顯示,如果我們這時候想測試數據有沒有顯示正確的話。上面的這些基礎API就做不到了,這就需要藉助Espresso的intent庫來支持了。
1.首先需要在build.gradle的dependencies中增加如下依賴:
androidTestCompile('com.android.support.test.espresso:espresso-intents:2.2.2', {
exclude group: "com.android.support"
})
2.替換ActivityTestRule爲IntentsTestRule
IntentsTestRule使得在UI功能測試中使用Espresso-Intents API變得簡單。該類是 ActivityTestRule的擴展,它會在每一個被 @Test 註解的測試執行前初始化 Espresso-Intents,然後在測試執行完後釋放 Espresso-Intents。被啓動的 activity 會在每個測試執行完後被終止掉,此規則也適用於 ActivityTestRule。
3.將IntentsTestRule的第三個構造參數設置爲false,表明不直接啓動activity
@Rule
public IntentsTestRule<ExtractSuccessActivity> mRule = new IntentsTestRule<>(ExtractSuccessActivity.class, true, false);
4.在啓動前將需要傳遞的參數放置到Intent對象中,然後啓動activity
@Before
public void setUp() {
Object data = ...;
Intent intent = new Intent();
intent.putExtra("data", data);
mRule.launchActivity(intent);
}
下面以團隊版App中提現成功這個頁面爲例,測試代碼如下:
@RunWith(AndroidJUnit4.class)
public class ExtractSuccessActivityTest {
private static final String BANK_NAME = "農業銀行";
private static final String CARD_NUM = "6225757538967564";
private static final double AMOUNT = 200;
private static final String EXPECT_AMOUNT = "¥200.00";
private static final String EXPECT_CARD_INFO = "農業銀行(7564)";
@Rule
public IntentsTestRule<ExtractSuccessActivity> mRule = new IntentsTestRule<>(ExtractSuccessActivity.class, true, false);
@Before
public void setUp() {
Bank bank = new Bank();
bank.setName(BANK_NAME);
BankCard card = new BankCard();
card.setCardNum(CARD_NUM);
card.setBank(bank);
Intent intent = new Intent();
intent.putExtra("param_bank_card", card);
intent.putExtra("param_amount", AMOUNT);
mRule.launchActivity(intent);
}
@Test
public void testDataShow() {
onView(withId(R.id.txt_extract_amount)).check(matches(withText(EXPECT_AMOUNT)));
onView(withId(R.id.txt_card_info)).check(matches(withText(EXPECT_CARD_INFO)));
}
}
三、跨進程測試
如果有這樣一個更改頭像的場景:用戶點擊更改頭像按鈕後,會調用系統自帶的相機進行拍照,然後再回到app提交拍好的照片。在這種場景下,我們需要從自己的app跳轉到其他的app,這種跳轉在實際業務中是很常見。
Espresso並沒有對這種跨app的交互測試提供支持,我們無法在測試代碼中通過espresso提供的api獲取到非自己app的其他app的UI元素,這時候就需要用到android提供的UI Automator來進行自動化測試了。
需要添加UI Automator的依賴:
dependencies {
...
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
}
採用UI Automator的過程如下:
1.獲得一個UiDevice對象,代表我們正在執行測試的設備。該對象可以通過一個getInstance()方法獲取,入參爲一個Instrumentation對象:
UiDevice mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
2.通過findObject()方法獲取到一個UiObject對象,代表我們需要執行測試的UI組件:
public UiObject findObject(UiSelector selector) {
return new UiObject(this, selector);
}
從如上聲明可以看出,findObject()方法接受了一個UiSelector對象,返回了我們需要的UiObject對象。在這裏,UiSelector類似於Espresso中的Matcher,也是指定了某種匹配規則,UI Automator會按照UiSelector指定的規則從當前UI上進行控件的查找。不同於Espresso的是,如果找到多個滿足規則的控件,則會返回第一個控件。如果沒有控件滿足當前指定的規則,則會拋出一個UiAutomatorObjectNotFoundException異常。
和Espresso類似,我們可以通過ID、text等屬性來進行控件的查找,同時也可以指定目標控件的類型。可以指定一個規則,也可以通過鏈式調用指定多個規則。比如:
UiObject mCameraSureBtn = mDevice.findObject(new UiSelector().resourceId("com.android.camera:id/v6_btn_done")
.className("android.widget.ImageView"));
這行代碼的UiSelector構建就是採用瞭如下兩個組合規則:
- 控件ID爲"com.android.camera:id/v6_btn_done",這個ID是從某個MIUI版本系統的系統相機獲取的,對應於拍照按鈕;
- 控件類型爲ImageView。
3.對該UI組件執行一系列操作
UiObject提供了一系列方法用來執行各種各樣的操作。比如:
- click():點擊控件中心;
- dragTo():拖動控件到指定位置;
- setText():對可輸入控件設置文本;
- swipeUp():對控件執行上滑操作。類似地,swipeDown(), swipeLeft()和swipeRight()可以執行相應的操作
4.檢查結果
執行一系列操作之後,我們需要對操作的結果進行驗證了! 對於結果的驗證,我們可以使用之前說到的一系列Assert方法了。比如說,我們需要檢測某個控件的文字:
assertEquals(TargetText, mUiObject.getText())
總結
Espresso這個框架整個流程學習下來,發現學習成本還是挺大的,感覺只有懂Android開發的人員才能用Espresso寫出測試代碼來,不但只支持Android App測試,而且還只能用Java語言來編寫,這是一個弊端,但不可否認的是espresso的功能還是蠻強大的,因爲其底層是Android API支持的,所以在可擴展性和測試速度方面還是比其他幾款自動化測試框架強大不少。所以在選擇測試框架時還是需要根據測試的業務來權衡每款測試框架的優缺點,沒有哪款框架敢說它什麼功能都能滿足。