解放雙手 - Android 開發應該嘗試的 UI 自動化測試

玉剛說發表於2018-08-06

本文由玉剛說寫作平臺提供寫作贊助

原作者:卻把青梅嗅

版權宣告:本文版權歸微信公眾號玉剛說所有,未經許可,不得以任何形式轉載

困境

接下來我將說到這種情況並非個例——作為一個Android開發者,當我實現了一個介面的一些功能,或者對介面上某些功能進行了修改,我該如何去查收我想要的結果呢?

最簡單的方式就是直接編譯執行App,通過自己的操作對介面進行互動,從個人的視覺效果上進行功能的檢查,比如我實現了一個RecyclerView,我就開啟介面,看看這個列表是否正確顯示在了介面上。

不久之後,我覺得某些地方程式碼不是很好,於是我改了一些程式碼,我怕會出現問題,於是為了保證專案能夠不出問題(至少是避免低階的錯誤),我選擇再次編譯執行,驗收結果。

再深入一點,如果每個版本釋出前都需要這麼多次測試,或者每當我們簡單修改了一下程式碼,就需要更多次重複進行以上步驟,並檢測結果,來來往往,反反覆覆,實在令人乏味。

I choose to die

也許, UI自動化測試是一勞永逸解決這個問題的方案之一。

UI自動化測試簡介

充滿熱情,一腔熱血,說學就學,我行我上。

相信我,不要這樣,這和學習或者框架不一樣,UI自動化測試是一個專業技能。不信的話,請參考一下各大機構對於測試工程師的培訓週期,系統性走一遍全日制要幾個月,閒暇時間學習?學不完的,而且,沒必要。

Android官方文件的概述,AndroidStudio提供了幾種UI測試工具供開發者使用。

事實上UI的自動化測試工具很多,但對於Android開發者來講,掌握其中的1至2項,就足以在UI測試領域立足,本文僅簡單介紹基礎的幾款工具以拋磚引玉。

1. Monkey

Monkey是Android SDK自帶的測試工具,在測試過程中會向系統傳送偽隨機的使用者事件流,如按鍵輸入、觸控式螢幕輸入、手勢輸入等),實現對正在開發的應用程式進行壓力測試,也有日誌輸出。實際上該工具只能做程式做一些壓力測試,由於測試事件和資料都是隨機的,不能自定義,所以有很大的侷限性。

2. Instrumentation

Instrumentation是早期Google提供的Android自動化測試工具類,雖然在那時候JUnit也可以對Android進行測試,但是Instrumentation允許你對應用程式做更為複雜的測試,甚至是框架層面的。通過Instrumentation你可以模擬按鍵按下、抬起、螢幕點選、滾動等事件。Instrumentation是通過將主程式和測試程式執行在同一個程式來實現這些功能,你可以把Instrumentation看成一個類似Activity或者Service並且不帶介面的元件,在程式執行期間監控你的主程式。缺點是對測試人員來說編寫程式碼能力要求較高,需要對Android相關知識有一定了解,還需要配置AndroidManifest.xml檔案,不能跨多個App。

3. UiAutomator

UiAutomator也是Android提供的自動化測試框架,基本上支援所有的Android事件操作,對比Instrumentation它不需要測試人員瞭解程式碼實現細節(可以用UiAutomatorviewer抓去App頁面上的控制元件屬性而不看原始碼)。基於Java,測試程式碼結構簡單、編寫容易、學習成本,一次編譯,所有裝置或模擬器都能執行測試,能跨App(比如:很多App有選擇相簿、開啟相機拍照,這就是跨App測試)。缺點是隻支援SDK 16(Android 4.1)及以上,不支援Hybird App、WebApp。

4. Espresso

Espresso是Google的開源自動化測試框架。相對於Robotium和UIAutomator,它的特點是規模更小、更簡潔,API更加精確,編寫測試程式碼簡單,容易快速上手。因為是基於Instrumentation的,所以不能跨App。

以上這些工具的概述,節選引用自知乎:Android 手機自動化測試工具有哪幾種?

如何入門?

UI的自動化測試的是一個複雜的系統,所謂望山跑死馬,作為Android開發者,我們想要通過閒暇的時間,期望短期能夠精通UI自動化測試是不現實的,但是每次都執行app手動測試又顯得很蠢,最好的方式,是通過了解並學習一個經典的UI測試工具,在瞭解到UI自動化測試的好處之後,再選擇繼續深入還是功成身退

有心的同學已經注意到了,上文中最後介紹的那個Espresso怎麼這麼眼熟呢?確實如此,在AndroidStudio2.2版本之後,在新建的專案中,AndroidStudio會預設新增Espresso的依賴。

這樣看來,Espresso顯然是一個不錯的選擇。正如Google所希望的,當Android的開發者利用Espresso寫完測試用例後,能一邊看著測試用例自動執行,一邊享受一杯香醇的Espresso(意式咖啡)。

Espresso學習指南

沒事走兩步

Google官方希望我們通過Espresso減少重複的勞動,那麼這所謂的UI自動化測試效果如何呢,正所謂手下見真章,我們來看一下Google的todo App的測試程式碼執行時的效果:

UI自動化測試效果

Espresso的原理是,通過測試程式碼模擬使用者對UI元素的操作,之後再校驗(verify)操作後的結果,和我們人為操作不同,Espresso能夠在短時間內測試所有的case,正如你所見的一樣。

我們不禁這樣想,如果一個介面涉及到很多的操作,沒有Espresso測試程式碼之前,每次修改,工作的責任感需要讓我自己先跑一遍所有功能,然後才敢打包扔給QA,但是如果我寫好了自動化測試程式碼,是不是意味著每次改完程式碼,只需跑一遍測試程式碼就代替了之前的手工操作呢?

testEnd

如您所見,本次測試了一個介面19個不同的操作,整個自動化過程共花費了4m34s,但在這個過程中我可以衝一杯咖啡,或者看看技術部落格,甚至是發呆——我愜意地得到了期冀的結果

如果我負責的功能模組所有介面,都覆蓋了這樣的測試程式碼,多好啊......——如果螢幕面前的您,有學習藉助自動化測試工具偷懶的想法,請堅持閱讀下去,以一個開發人員而非專業測試人員的視角,分享學習自動化測試的經驗,這也正是本文的目的。

主旨

本文的目標是,以自己的學習經歷為基礎,為想要學習Espresso(或者有這個想法)的同學,提供一個系統性的規劃和建議

這意味著,本文不會去詳細闡述每個API的使用,於我而言,這些應該交給官方文件去闡釋,當然,對於API而言,我也不認為能夠講述的比官方文件更優秀。

我會通過一些簡單的測試程式碼闡述UI自動化測試所需要的一些基礎或思想,但是程式碼本身不應該是本文的重點,我更希望,當您讀完本文,您能有啊,原來Espresso的UI測試應該這麼學之感——而不是哦,原來這個API是這麼用的

如果將本文的定義類比於該知識體系目錄或者導航,我覺得再恰當不過。

如何學習Espresso

我的建議是按照以下步驟進行學習:

1. Fork Google官方的Demo程式碼,執行並感受測試程式碼的威力

Google官方的todo案例地址:

https://github.com/googlesamples/android-architecture

我們拉下來程式碼後,選一個您比較感興趣的分支,比如比較簡單一點的todo-mvp分支,這個分支中程式碼的實現僅僅使用了MVP的架構,學習起來並不複雜。

我們來看一下專案的目錄結構:

解放雙手 - Android 開發應該嘗試的 UI 自動化測試

其中androidTest和androidTestMock都是UI測試的程式碼,我們先右鍵點選androidTest資料夾,run該資料夾下的所有UI測試case。

解放雙手 - Android 開發應該嘗試的 UI 自動化測試

選中裝置後,AndroidStudio就會編譯並自動打包(注意實際上此處的測試打包和實際生產的打包並不一樣),然後自動在裝置上執行所有的測試case——就和上文中的效果一樣。

看到這裡,我們不僅感嘆測試程式碼的強大,不要沉迷於此,我們繼續第二步:

2. 閱讀Espresso的官方文件

如果點進去看測試程式碼的話,我們會比較懵逼,因為我們對於Espresso的使用一無所知,那麼接下來我們要去做的,就是閱讀Espresso的官方文件了:

Espresso官方文件:

https://developer.android.com/training/testing/espresso/basics

Espresso官方文件中文翻譯:

https://lovexiaov.gitbooks.io/official-espresso-doc/content/

中文翻譯的gitbook的確不好找,在此不僅感嘆UI自動化測試的小眾性,特別感謝譯者lovexiaov,沒有你的分享精神,我就只能考慮自己去硬啃英文文件了。

實話說,中文的文件部分翻譯不夠準確,建議大家,有能力還是看英文原版,我更建議大家中英文對照學習。

這一步,我們不需要深入學習並使用文件中列舉的所有API,只需要參照文件看得懂todoApp中測試程式碼的用意就行了。

3. 付諸實踐

當我們參照API文件,並且能夠基本看得懂demo程式碼中,大部分測試case想要幹什麼,我們接下來就可以嘗試付諸實踐了。

實踐

接下來我將會用簡單的程式碼闡述Espresso的簡單使用。

1.Hello Espresso!

來一個最簡單的demo,當我們點選一個Button,讓介面某個TextView顯示HelloEspresso的文字內容。

我們忽略xml佈局的實現,簡單看一下Activity中的部分Java程式碼:

public void onViewClicked(View view) {
      switch (view.getId()) {
          case R.id.button:
              // 點選button後,textview顯示hello espresso!
              textView.setVisibility(View.VISIBLE);
              textView.setText("hello espresso!");
              break;
      }
}
複製程式碼

非常簡單,測試程式碼自然也淺顯易懂:

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

    @Rule
    public ActivityTestRule<MainActivity> rule =
            new ActivityTestRule<>(MainActivity.class);

    @Test
    public void clickTest() {
        //檢驗:一開始,textView不顯示
        onView(withId(R.id.textView))
                .check(matches(not(isDisplayed())));

        //檢驗:button的文字內容
        onView(withId(R.id.button))
                .check(matches(withText("修改內容")))
                .perform(click());  //操作:點選按鈕

        //檢驗:textView內容是否修改,並且變為可見
        onView(withId(R.id.textView))
                .check(matches(withText("hello espresso!")))
                .check(matches(isDisplayed()));
    }
}
複製程式碼

程式碼非常簡單,邏輯也很清晰,我們測試的思路是,找到我們要操作的介面元素,然後操作該介面元素,然後校驗UI的變化。

在這個測試中,當我們點選了button後,會校驗:介面上TextView變為可見,同時附有“hello espresso!”的內容——如果測試失敗了,說明我們預期的操作並未得到預期的結果,我們就需要去檢查程式碼了。

2.模擬使用者的登陸操作

接下來我們跳轉另外一個場景,稍微複雜一點,介面上有一個EditText,負責使用者輸入賬號,還有一個Button,負責登入,還有一個TextView,當使用者點選Button後,TextView會顯示登入成功並且清空輸入框

public void onViewClicked(View view) {
      switch (view.getId()) {
          case R.id.button2:
            // 登陸成功並且清空輸入框
            textView.setVisibility(View.VISIBLE);
            textView.setText("登入成功");
            editText.setText("");
            break;
      }
}
複製程式碼

我們可以這樣補充測試Case:

@Test
    public void loginTest() throws Exception {
        //先清除editText的內容,然後輸入,然後關閉軟鍵盤,最後校驗內容
        //這裡如果要輸入中文,使用replaceText()方法代替typeText()
        onView(withId(R.id.editText))
                .perform(
                    clearText(),
                    replaceText("username"),
                    closeSoftKeyboard()
                )
                .check(matches(withText("username")));

        // 操作:點選Button
        onView(withId(R.id.button2))
                .perform(click());

        //校驗:textView的內容和可見
        onView(withId(R.id.textView))
                .check(matches(withText("登入成功")))
                .check(matches(isDisplayed()));

        //校驗:editText的文字內容(被清空)和hint
        onView(withId(R.id.editText))
                .check(matches(withText("")))
                .check(matches(withHint("請輸入賬戶名")));
    }
複製程式碼

大功告成——和基本案例基本差不多,都是通過簡單的對View的操作+校驗完成了UI的測試程式碼編寫。

看起來我們已經熟悉了Espresso的使用套路,我已經有信心在真實的專案中應用它了。

3.熟練使用Espresso進行UI自動化測試

正所謂行百里路半九十,當我們將看起來並不複雜的Espresso應用在真實的專案中時,我們馬上就會遇到一個很嚴重的問題,那就是:

並非所有的UI操作都是同步響應的!

Espresso進行一個簡單的同步功能測試並不難,比如我們點選了一個Button,點選後改變對應某個TextView的內容,這很簡單。但實際正常開發中,這種簡單的邏輯測試是很少見的,相反,我們需要測試的是各種各樣的非同步測試,比如:

情景一:點選進入Activity,網路請求資料載入,成功後資料展示在介面上。 情景二:點選進入Activity,獲得快取,網路請求資料載入,成功後資料展示在介面上,處理快取。 情景N : ......

假設這樣一個簡單的網路請求測試:

@Test
public void testHttp() {
   // 我們請求網路資料,成功後讓TextView顯示"網路請求成功"
   // 同時ImageView從不可見變為可見

    //如果我們直接檢查是不是請求到了資料
    onView(withId(R.id.textView)).check(matches(withText("網路請求成功!")));
    onView(withText(R.id.imageView)).check(matches(isDisplayed()));
}
複製程式碼

如果我們直接測試,那麼很大概率會報錯,因為在我們要測試資料是否展示在UI上時,網路資料很有可能還沒有獲取到。

這很難處理,因為我們不知道資料到底什麼時候才能獲取到,有同學抖了個機靈,說我們可以這樣:

 @Test
public void testHttp() {
    // 我們一進來就先讓他等待5秒,等資料載入完畢再檢查UI
    Thread.sleep(5000);

    // 5秒結束,我們檢查是不是請求到了資料
    onView(withId(R.id.textView)).check(matches(withText("網路請求成功!")));
    onView(withText(R.id.imageView)).check(matches(isDisplayed()));
}
複製程式碼

這樣可以實現嗎,這個大概率真的可以,但是這種測試顯然問題很多,因為網路情況是在不斷變化的,也許0.5s就能獲取網路資料,也有可能數十秒後才能獲取,這樣前者導致我們浪費了4.5s的時間,後者在網路狀態屬於正常的時候測試結果失敗,這都是我們不願看到的結果。

我們更希望在獲取到網路資料之後,立即進行下一步的測試,因此我們需要對網路資料的獲取情況進行監聽。

但是問題來了,如何在UI測試程式碼中,對真實的網路狀態進行監聽呢?

這個問題難倒了我,好在Google的工程師們已經在todo的demo中提供了一種解決的方式,我們來看一看官方的方案。

4.非同步操作的測試思路

Google官方提供了IdlingResource以供開發者進行UI的非同步測試,對於IdlingResource的解釋說明,我們可以參照官方文件:

https://developer.android.com/training/testing/espresso/idling-resource

或者中文文件對於IdlingResource的解釋:

https://lovexiaov.gitbooks.io/official-espresso-doc/content/chapter6.html

我不會用大段程式碼闡述如何使用,它的基本原理是:在生產程式碼中,定義一個Flag(標記),當開始非同步請求前,修改Flag的狀態,當網路請求結束後,將Flag的狀態重置,這時候Flag狀態修改的事件會被髮送通知給註冊的物件(比如測試程式碼):

@Before
public void setUp() throws Exception {
    idlingresource = activityRule.getActivity().getIdlingresource();
}

@Test
public void onLoadingFinished() throws Exception {
    //  不再需要這樣的程式碼
    //  Thread.sleep(5000);

    // 註冊非同步監聽,此時測試會被掛起
    // 當網路請求結束後,生產程式碼中Flag狀態的改變,會繼續執行測試程式碼
    Espresso.registerIdlingResources(idlingresource);

    // 繼續執行程式碼
    onView(withId(R.id.text))
            .check(matches(withText("success!")));
}

@After
public void release() throws Exception {
    // 當然,我們需要在測試結束後取消註冊,釋放資源
    Espresso.unregisterIdlingResources(idlingresource);
}
複製程式碼

這種行為的好處不言而喻,它能夠在非同步結束之後馬上執行接下來的測試程式碼,從效果上來說,相比Thread.sleep(5000);不知好了多少。

它的缺點也很明顯,那就是測試程式碼實際需要依賴對生產程式碼進行配置(本文中並未展示,請參考todoDemo或者這篇文章)。

難道就沒有更好的解決方案嗎?當然有,Google對此的建議是,重構專案程式碼(比如增加product flavors,或者通過依賴注入等等),使其變得可測試性——到這一步,請慎重考慮,因為這已經涉及到專案的架構以及專案管理的層級上了。

5.更多

實際上,Espresso在應用在實際專案中,需要我們去面對的問題並不少,絕大多數情況下,這些問題都能夠通過搜尋引擎然後親自實踐去解決—— 你絕不是一個人在戰鬥。

小結——堅持還是放棄?

很多同學都瞭解單元測試UI自動化測試的重要性,但是這些工具需要不菲的時間成本,那麼它們真的還有必要去學習嗎?

有位同學舉手了,他同樣表示——有時雖然修改一個小功能,需要開發者多次手動測試很麻煩,但是也並非不可接受,至少上班時,在專案編譯執行期間,我可以切換網頁,看看新聞摸摸魚

解放雙手 - Android 開發應該嘗試的 UI 自動化測試

當然還有這樣的情況,作為一個開發人員,即使我學會了自動化測試,我也不一定有機會去應用它,直接嘗試應用在一個已經完善的成熟專案中,是不現實的;這樣的話,會不會出現,學會了但根本用不到的窘境?

每個人都不能保證將來還是否還會遇到曾經的問題,如果遇到了,我該怎麼做,是選擇繼續躲避,還是一勞永逸;而且,即使學會了,如何保證這能成為我核心競爭力的一部分,而不是學會了卻用不到,最終被慢慢忘記?

的確,我們有時的確沒有必要為工作做出額外的付出(思考和實踐),萬一搞砸了,反而不如不做。但是我要闡述的一點是:不做並不意味著問題被解決了——你只是暫時避開了它,而下一次遇到它的時候,你仍需去面對這個困境;並且,如果將測試任務交給了程式碼,摸魚的時候豈不更加輕鬆?正所謂,授人以漁,勞神費力。 而——

解放雙手 - Android 開發應該嘗試的 UI 自動化測試

我的思路

言歸正傳,對於如何實踐自動化測試,我的方式是對個人的一些工具程式碼進行UI自動化測試的覆蓋,在進一步完善自己的工具同時,深入瞭解Espresso。

筆者對於Espresso的經驗所得來自於自己的**Github這個工具,它是Android的一個響應式圖片選擇器**,因此每次釋出新版本筆者都需要自己測試UI,而UI自動化測試無疑可以減少這些重複的操作。 ——這個庫UI測試的更詳細過程並非本文的重點,我在另一篇文章中去闡述了它:

全副武裝!AndroidUI自動化測試在RxImagePicker中的實踐歷程

一千個觀眾眼中有一千個哈姆雷特,只要感興趣,總能找到適合自己的方式,本文所講述的Espresso僅僅是UI自動化測試這門專業技術的一部分,但我認為它很契合Android開發者,並藉助它為自己的UI介面進行白盒測試(也有朋友稱Espresso為灰盒測試),正如官方文件所描述的(下為譯文):

Espresso 的使用群體為堅信自動化測試是開發週期中必不可少的一部分的開發者。雖然它可被用來做黑盒測試,但 Espresso 會在對被測程式碼庫熟悉的人手中火力全開

在我們感嘆AndroidStudio預設提供的依賴庫中,JUnit4可以讓我們通過單元測試保證最小模組程式碼的可靠性,ConstraintLayout讓我們減少大量佈局巢狀的同時慢慢拋棄了RelativeLayout的同時,請也不要忽視Espresso,真正瞭解了它並付諸實踐,便會對它強大的UI自動化測試功能愛不釋手。

解放雙手 - Android 開發應該嘗試的 UI 自動化測試
歡迎關注我的微信公眾號「玉剛說」,接收第一手技術乾貨

相關文章