何謂測試?在Android上該如何實作?- Part 1 (單元測試)

York
6 min readJun 25, 2018

--

測試是為了確保程式執行的結果與預期相同,開發 App 時也有許多情況下會促使開發人員先寫測試再寫產品程式碼。這邊介紹一些我學習測試的基本概念與在 Android 上的實作 Unit Test 及 UI Test 的簡單範例,希望能讓剛學習測試的人能有較明確概念。

Test 的意義與優點

Test (測試)是開發 App 的一部分,常常我們在開發完一個功能或流程後,需由 Test 來確認實際結果是否與預期的相同。有 Test 的好處在於能夠得到快速的回饋,以確保開發新功能時不用擔心是否會影響到原有的部分,或是回頭重構時確保程式碼的正確性,譬如我們預先想好某個 function 所要做的事並定義他的參數與回傳值,如此一來,就能針對該function寫一個 Test case ,確保在某個流程中呼叫 function 並丟入想要的參數後,他的回傳值與預期的相同。有了 Test case 後,無論之後要如何修改function內部實作的內容,我們都能藉由執行 Test case 快速得知結果是否與預期一致。

Testing Pyramid

如同上圖的金字塔,一個 App 應包含 Small Tests, Medium Tests, Large Tests,通常建議測試要包含70%的 small tests,20%的 medium tests,10%的 large tests 。

  • Small tests:對應的是 Unit Tests ,可能是針對某個 function 或是 class 的test。
  • Medium tests:可能是 components 之間的 integration tests ,需要在emulator或是 real devices 上執行。
  • Latge test:藉由完成整個 UI workflow 的 UI test,確保關鍵的 user task 和預期運作的一樣。

從最基本且重要的單元測試開始!

一個程式要最建立最多的測試就屬 unit test 了。unit tests 通常使用在最小單位的程式碼中,可能是 class, method 或 component。當要驗證某個程式碼的邏輯是否正確時,便應該使用 unit tests,因此為程式的每個重要邏輯建立unit test 便能確保可靠性。

以我之前做的找咖啡App來示範建立unit test。假設我要在MapPageFragment顯示距離最近的咖啡廳,那麼依序要做到以下兩件事:

  1. 將傳進Fragment的所在位置郵遞區號的資料轉成用在查詢縣市咖啡廳API的city id。
  2. 取得某縣市的所有咖啡廳資料後再依照距離排序。

為了讓每項工作的程式碼區分清楚,我以MVP架構分成 MapPageView、MapPagePresenter 和幾個 model class。由於 presenter 的每個方法都代表view 與 model 之間互動的流程,因此很適合用unit test檢查是否符合預期的結果。

依照要達成的目標,在presenter設計三個方法:

  • getCityIdFromPostalCode():將郵遞區號轉成city id。
  • getCityCafes():利用city id 於後端API取得某縣市的所有咖啡廳。
  • sortSelectiveCafe():將所有咖啡廳依照距離排序。

以上方法的邏輯分別是GetCityId、GetCityCafes、SortByDistance三個model class所負責,而我可以先設計測試案例檢查他們執行的結果是否正確,因此會建立以下兩個unit tests:

@Test
fun getCityIdFromPostalCode() {
val getCityId = GetCityId()
val postalCode = 500 // 彰化的郵遞區號
Assert.assertSame("changhua", getCityId.cityId(postalCode))
}
@Test
fun getSelectiveCafes() {
val lastLocation = Location()
lastLocation.latitude = 23.0200729
lastLocation.longitude = 120.2226109

Assert.assertSame(43, getCityCafes.cafes("changhua", lastLocation).size)
}@Test
fun sortCityCafes() {
val cityCafes = ArrayList<Cafe>()
// Cafe建構子需傳入"名稱"、"開店時間"、"結束時間"
cityCafes.add(Cafe("好想咖啡廳", "10:00", "20:00", 10680))
cityCafes.add(Cafe("等一杯咖啡廳", "11:00", "21:00", 5000))
cityCafes.add(Cafe("雲端咖啡廳", "9:00", "20:00", 25460))
// 將儲存咖啡廳的 list 傳給 SortByDistance class 排序
sortByDistance.sort(selectiveCafes)

// 將對或錯的排序結果傳入isDistanceIncremental
val isDistanceIncremental = selectiveCafes[0].distance < selectiveCafes[1].distance && selectiveCafes[1].distance < selectiveCafes[2].distance

Assert.assertSame(true, isDistanceIncremental)
}

由以上的程式碼可以看出設計測試案例就是用 Asser.asserSame(expectValue, actualValue) 分別傳入期望的正確值和實際值來判斷執行結果是否符合預期,當然以上程式碼有簡化過,實際上會更複雜一點。但到目前為止我們已經體會到寫測試最重要的目的在於強迫自己把程式碼結構寫的更加職責分明。

以上就是寫測試最基本的且重要的單元測試,未來會陸續介紹實際寫測試時會用到的框架如 Roboletric, Mockito 等等。

--

--