Tina Tang's Blog

在哪裡跌倒了,就在哪裡躺下來

0%

Android筆記(30)-測試ViewModel和LiveData

在先前的程式碼研究室中,您已學會如何使用 ViewModel 處理商業邏輯,以及使用 LiveData 處理 reactive UI。在本程式碼研究室中,您將學習如何編寫單元測試(unit tests),以檢查 ViewModel 程式碼是否正常運作。

學習目標

  • 學會如何設定 LiveData 測試。
  • 學會如何測試 LiveData 本身。
  • 學會如何測試已完成轉換(transformed)的 LiveData
  • 學會如何在單元測試中觀察(observe) LiveData

建構項目

  • 編寫 ViewModelLiveData 單元測試(unit tests)。

建立單元測試目錄

單元測試一律位於 test 目錄中:

  1. 從「Android」切換至「Project」。
  2. 按一下第一個下拉式選單,然後依序點選「app」->「src」。
  3. 用滑鼠右鍵按一下「src」,然後選取「New」->「Directory」。
  1. 輸入並選取 test/java
  1. 現在會在專案結構中看到「src」->「test」目錄。
  1. 用滑鼠右鍵按一下「java」目錄,然後依序選取「New」->「Package」。
  1. 在視窗中輸入 com.example.cupcake,然後按下 Return 鍵。
  1. 最後,建立名為 ViewModelTests.kt 的新 class。用滑鼠右鍵按一下 com.example.cupcake,然後依序選取「New」->「Kotlin Class/File」。
  1. 在出現的視窗中輸入 ViewModelTests,然後從下拉式選單中選取「Class」。

新增必要的 dependencies

將下列 dependencies 新增至專案:

1
2
testImplementation 'junit:junit:4.+'
testImplementation 'androidx.arch.core:core-testing:2.1.0'

接著同步(sync)專案。


編寫和執行 ViewModel 測試

編寫單元測試

讓我們先從簡單的測試開始。當我們在裝置(device)或模擬器(emulator)上與 app 互動時,首先會選取杯子蛋糕(cupcakes)的數量(quantity)。因此,我們會OrderViewModel 中測試 setQuantity() 方法,並檢查 quantity LiveData 物件的 value

我們要測試的 quantity 變數是 LiveData instance。測試 LiveData 物件需要執行額外步驟,因此,我們所新增的 dependency 就能派上用場。只要 value 有變更,我們就會使用 LiveData 更新 UIUI 於「主執行緒(main thread)」上運作。如果您不熟悉執行緒(threading)並行(concurrency)的概念,別擔心,我們會在其他程式碼研究室中深入介紹。

就 Android 應用程式而言,請暫時將主執行緒(main thread)視為 UI 執行緒(thread)。向使用者顯示 UI 的程式碼會在此執行緒(thread)上運作。除非另有指定,否則單元測試(unit test)會假設所有項目皆在主執行緒(main thread)上運作。不過,由於 LiveData 物件無法存取主執行緒(main thread),因此必須明確指出 LiveData 物件不得呼叫主執行緒(main thread)

  1. 如要指定 LiveData 物件不得呼叫主執行緒(main thread),我們需在每次測試 LiveData 物件時提供專用測試規則(test rule)
1
2
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
  1. 現在可建立名為 quantity_twelve_cupcakes() 的 function。在 method 中,建立 OrderViewModel instance。
1
2
3
4
@Test
fun quantity_twelve_cupcakes {

}
  1. 在本測試中,您需確認 OrderViewModel 中的 quantity 物件在 setQuantity 呼叫時已更新。但在呼叫任何 method 或處理 OrderViewModel 中的任何資料前,請注意測試 LiveData 物件的 value 時,必須先觀察(observed)物件,才能發出變更。方法很簡單,只要使用 observeForever method 即可。呼叫 quantity 物件的 observeForever method 。這個 method 需要 lambda 運算式,但可以留空(empty)。

  2. 然後呼叫 setQuantity() method,將 12 傳入做為參數。

1
2
3
4
5
6
7
8
9
// 在 method 中,建立 OrderViewModel instance
val viewModel = OrderViewModel()

// 確認 OrderViewModel 中的 quantity 物件在 setQuantity 呼叫時已更新
// 測試 LiveData 物件的 value 時,須先使用 observeForever method 觀察(observed)物件,才能發出變更
viewModel.quantity.observeForever {}

// 呼叫 setQuantity() method,將 12 傳入做為參數
viewModel.setQuantity(12)
  1. 我們可以放心推論 quantity 物件的 value 為 12。請注意,LiveData 物件本身並不是 value。value 包含在名為 value 的屬性中。指定以下斷言(assertion):
1
assertEquals(12, viewModel.quantity.value)
  • 請注意,嘗試呼叫 assertEquals() 方法時,文字會先顯示為紅色。這是因為找不到這個方法的宣告,所以您必須 import 宣告(option+enter),然後選擇來自 org.junit.Assert package 的選項。

Test code 的編寫方式如下:

1
2
3
4
5
6
7
@Test
fun quantity_twelve_cupcakes() {
val viewModel = OrderViewModel()
viewModel.quantity.observeForever {}
viewModel.setQuantity(12)
assertEquals(12, viewModel.quantity.value)
}

補充:斷言(assertion)是一種放在程式中的一階邏輯(如一個結果為真或是假的邏輯判斷式),目的是為了標示與驗證程式開發者預期的結果-當程式執行到斷言的位置時,對應的斷言應該為真。若斷言不為真時,程式會中止執行,並給出錯誤訊息。
點此查看常見的斷言(assertion)

執行單元測試

  1. 點選 Run Test 執行 ViewModelTests
  1. 查看測試結果,顯示 Tests passed 即代表測試通過。

執行測試!恭喜!您剛剛已編寫第一個 LiveData 單元測試(unit test),這是 Modern Android Development 的重要技能。這種方式並未測試到大部分的商業邏輯,因此我們要設計較為深入的測試。

計算訂單價格是 OrderViewModel 的主要功能之一。當我們選取杯子蛋糕數量,並選取取貨日期時,就會執行此功能。價格計算是以 private method 進行,因此我們的測試無法直接呼叫此 method。只有 OrderViewModel 中的其他 method 可以進行呼叫。這些是 public method,因此我們會呼叫此類 method 觸發價格計算作業,以便確認價格值是否符合預期。

最佳做法

選取杯子蛋糕數量及日期時,價格將會隨之更新。儘管兩者都應進行測試,但我們通常建議針對單一功能進行測試。因此,我們會針對各測試建立不同的方法:

  • 數量(quantity)更新時用於測試價格(price)的 function
  • 在更新日期(date)時用於測試價格(price)的 function

我們不希望測試結果因為另一項測試失敗而失敗。

  1. 建立名為 price_twelve_cupcakes() 的方法,並將其做為測試加上註解(加上 @Test)。

  2. 在方法中,建立 OrderViewModel instance,並呼叫 setQuantity() 方法,將 12 傳入做為參數。

1
2
val viewModel = OrderViewModel()
viewModel.setQuantity(12)
  1. 查看 OrderViewModel 中的 PRICE_PER_CUPCAKE 時,可看見每個杯子蛋糕的售價為 $2.00 美元。還可以看到每次 ViewModel 初始化時都會呼叫 resetOrder(),在此方法中,預設日期為今天的日期,PRICE_FOR_SAME_DAY_PICKUP$3.00 美元。因此,12 * 2 + 3 = 27。選擇 12 個杯子蛋糕後,我們預期 price 變數的 value 應為 $27.00 美元。接著做出斷言,假設 $27.00 美元的預期值等於 price LiveData 物件的 value。
1
assertEquals("$27.00", viewModel.price.value)

現在請執行測試,測試應會失敗!

測試結果顯示,實際值是 null。以下為相關說明。如果您查看 OrderViewModel 中的 price 變數,可看見:

1
2
3
4
val price: LiveData<String> = Transformations.map(_price) {
// Format the price into the local currency and return this as LiveData<String>
NumberFormat.getCurrencyInstance().format(it)
}

此範例是應在測試中觀察(observed)到 LiveData 的原因。系統會使用 Transformation 設定 price 的 value。基本上,此程式碼會使用我們指派給 price 的 value,並將其轉換成貨幣格式,使我們不必手動執行。但這個程式碼還有其他含意。轉換 LiveData 物件時,除非有必要進行呼叫,否則系統不會呼叫程式碼,而會將資源(resources)儲存到行動裝置(mobile device)。只有在觀察(observe)到物件變更時,系統才會呼叫程式碼。當然,此操作是在 app 中執行,但我們也需要為測試進行相同操作。

  1. 在測試方法中,請先新增下列程式碼,再設定數量(quantity):
1
viewModel.price.observeForever {}

測試程式碼的編寫方式如下:

1
2
3
4
5
6
7
@Test
fun price_twelve_cupcakes() {
val viewModel = OrderViewModel()
viewModel.price.observeForever {}
viewModel.setQuantity(12)
assertEquals("$27.00", viewModel.price.value)
}

如果現在進行測試,應可通過測試。