前言
最近在嘗試深入學習Android單元測試。
以前筆者對於單元測試的理解很簡單,做一個簡單的API測試獲取Server端數據,或者測試對應簡單的工具類和數據處理的邏輯。
經過這近一個月的碎片化學習,深深瞭解到了單元測試的重要性,想找個機會總結一下,但是網上很多前輩都已經做出了更精闢的總結:
更多理由不繫表,如果有機會,筆者會專門寫一篇關於對於單元測試淺陋的理解以及單元測試在開發中的重要性。個人認爲項目中添加單元測試的優勢有:
方便重構
節約時間
- 提升代碼設計
在AndroidStudio2.2+的版本,新建的Project中會自動爲開發者添加Espresso自動化測試庫的依賴:
這說明谷歌官方也十分推崇開發者在開發時通過Espresso進行自動化測試,從而提高開發效率,我們作爲Android開發者,也有必要去抽時間去了解一下其優勢.
順便提一句,AndroidStudio2.3+的版本中,細心的同學可以發現,新建的Project中會爲開發者添加ConstraintLayout(約束佈局)的依賴,該佈局大幅提升了xml文件UI繪製的速度,並且避免了多層佈局嵌套的苦手。有興趣的同學可以去了解一下。
筆者的Android單元測試相關係列:
Android 自動化測試 Espresso篇:簡介&基礎使用
Android 自動化測試 Espresso篇:異步代碼測試
一.簡介,我們爲什麼使用Espresso
Espresso 是在2013年的 GTAC 上首次提出,目的是讓開發人員能夠快速地寫出簡潔,美觀,可靠的 Android UI 測試。
舉個簡單的例子,我們這邊有一個功能,點擊界面Button,網絡請求加載圖片,正常開發人員想測試,需要:
點擊run -> gradle build -> install apk -> 手動點擊按鈕 ->本人監測結果
其實並不麻煩,但是考慮一下,如果每個版本發佈前都需要這麼多次測試,或者我們簡單修改了一下代碼,那麼我們需要再次進行以上步驟,並監測結果,來來往往,反反覆覆,實在是子子孫孫無窮匱也!
如果使用Espresso,我們只需要運行代碼,代碼會自動幫助我們執行這些流程(點擊按鈕)並且監測結果(是否加載到對應圖片):
可以看到上面實際上,安裝,進入界面,點擊按鈕,加載圖片,判斷結果,都是自動化的測試,不需要開發者去人爲監測。
其實相比較於單元測試,Espresso我個人理解更傾向於像自動化集成測試,因爲這個庫實質上是開發者進行測試的時候,在測試設備上面安裝了兩個apk(開發者一般的apk和AndroidTest apk),然後模擬操作(比如點擊按鈕,滑動列表等等)在界面上,將開發者預期的結果和實際的結果進行對比。
二.Espresso測試 對比 單元測試(UnitTest)
這和單元測試(UnitTest)實際上是有本質的區別的,首先我們並不推薦單元測試中有真正的網絡數據交互,通常我們的方式是本地mock一個模擬數據,然後進行測試。
其次,單元測試的功能一定是要單一的,並且是最基礎的一個單元,測試的功能越單一,耦合度越低越好。這樣如果我們需要測試一個複雜的功能,只需要把數個單一功能的模塊分別測試,只要這些小模塊都沒有問題,組合出來的複雜功能自然也就不會出現什麼問題了。
但是Espresso最強大的功能是UI界面的自動化測試,不可否認它支持異步請求(網絡請求,數據庫操作),但是成本過於高昂(需要大量的代碼,而且需要實現idlingResource接口,這個後文再提)。
同時,Espresso需要依賴Android設備.這將導致我們將花費更多時間在編譯apk和AndroidTest apk的安裝上(即使已經比一般的手動測試快了很多倍)。
最後,它的耦合度看起來確實很大,正常我們項目的需求不可避免的都需要網絡請求,那麼我們簡單的測試點擊按鈕,實際上還測試到了網絡請求,圖片加載等等,這樣一個測試用例,實際上耦合了很多的功能,這不符合單元測試的思想。
但是Espresso一無是處了嗎?並非如此。
轉機
我們前文提到了,Espresso最強大的功能就是UI自動化測試,這是其他單元測試框架達不到的,所以我們完全可以通過Espresso進行界面數據展示的功能測試。
簡而言之,我們可以讓Espresso處理它拿手的UI界面測試,而網絡請求等業務處理,我們可以交給其他測試框架去處理,比如Mockito(後文再講)。
這裏我們就要提到經典的MVP設計模式,MVP模式將數據的展示處理交給了View,業務代碼交給了Model,我們完全可以通過MVP模式,將測試代碼分開來測試,這樣我們的問題就解決了。
說了這麼多,不要好高騖遠,筆者的意思是Espresso是值得Android開發人員花費時間成本去學習的,接下來做一個簡單的案例,瞭解一下Espresso的簡單使用。
三.Hello Espresso!
我們先來看一下我們的需求:
很簡單,可以分爲兩個功能:
1,點擊登錄按鈕,顯示登陸成功Success,同時清空輸入框
2,點擊修改內容,顯示hello espresso!
1.基礎代碼
看一下基本代碼,很簡單:
xml,佈局:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".a_espresso.a01_simple.A01SimpleActivity">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/btn01"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="修改內容" />
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<EditText
android:id="@+id/et_01"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="請輸入賬戶名"
android:text="123456"
/>
<Button
android:id="@+id/btn02"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="登錄" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
Activity中對點擊事件的處理:
@OnClick({R.id.btn01, R.id.btn02})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.btn01:
//顯示hello espresso!
tvContent.setVisibility(View.VISIBLE);
tvContent.setText("hello espresso!");
break;
case R.id.btn02:
//登陸成功並且清空輸入框
tvContent.setVisibility(View.VISIBLE);
tvContent.setText("success");
et01.setText("");
break;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
2.測試代碼
在你的AndroidTest目錄下創建一個測試類,名字無所謂,但是爲了規範我起名爲Activity全名+Test:
比如MainActivity -> MainActivityTest
這樣一目瞭然,見名知意。
測試類代碼如下,有註解相信並不難理解:
@RunWith(AndroidJUnit4.class)
public class A01SimpleActivityTest {
@Rule
public ActivityTestRule<A01SimpleActivity> rule = new ActivityTestRule<>(A01SimpleActivity.class);
@Test
public void clickTest() {
//tvContent是否默認不顯示
onView(ViewMatchers.withId(R.id.tv_content))
.check(matches(not(isDisplayed()))); //是否不可見
//檢查btn01的text,然後執行點擊事件
onView(withId(R.id.btn01))
.check(matches(withText("修改內容")))
.perform(click());
//檢查tv內容是否修改,並且是否可見
onView(withId(R.id.tv_content))
.check(matches(withText("hello espresso!")))
.check(matches(isDisplayed()));
}
@Test
public void loginTest() throws Exception {
//先清除editText的內容,然後輸入,然後關閉軟鍵盤,最後校驗內容
//這裏如果要輸入中文,使用replaceText()方法代替typeText()
onView(withId(R.id.et_01))
.perform(clearText(), replaceText("你好 username"), closeSoftKeyboard())
.check(matches(withText("你好 username")));
//點擊登錄
onView(withId(R.id.btn02))
.perform(click());
//校驗內容
onView(withId(R.id.tv_content))
.check(matches(withText("success")))
.check(matches(isDisplayed()));
onView(withId(R.id.et_01))
.check(matches(withText(""))) //內容是否爲""
.check(matches(withHint("請輸入賬戶名"))) //hint內容是否爲"請輸入賬戶名"
.check(matches(withHint(containsString("賬戶名")))); //hint內容是否包含"賬戶名"
Thread.sleep(3000);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
需要注意的是,因爲我們實際上有兩個功能要測試,所以筆者建議最好每個功能分開寫一個Test方法。
比如該測試類中,clickTest()測試點擊顯示Espresso,loginTest()測試登陸功能。
當然所有功能寫一起也可以,但是這樣本身就不太符合我們對測試的定義,多寫幾個測試方法,保證每個方法只涉及一個功能。這樣才符合單元測試的粒度。
我們隨便選擇一個功能,以loginTest爲例,運行測試代碼(錄屏有所瑕疵,見諒!):
查看結果:
測試通過。
總結
本文介紹了Espresso的實用性,同時通過了一個簡單的入門案例,瞭解了Espresso的基本使用方式,如果對於Espresso某個方法不理解,還請查閱相關API文檔:
建議不熟悉的同學上手一遍,基本就能瞭解了,本身Espresso的方法可以稱得上是見文知意了。
本系列的所有代碼都已託管GitHub:
本文案例代碼:
下一篇文章,我們將對Espresso的異步網絡請求進行簡單的學習,歡迎關注。