在先前的程式碼研究室中,您已學會如何使用
ViewModel
處理商業邏輯,以及使用LiveData
處理 reactive UI。在本程式碼研究室中,您將學習如何編寫單元測試(unit tests),以檢查ViewModel
程式碼是否正常運作。
學習目標
- 學會如何設定
LiveData
測試。 - 學會如何測試
LiveData
本身。 - 學會如何測試已完成轉換(transformed)的
LiveData
。 - 學會如何在單元測試中觀察(observe)
LiveData
。
建構項目
- 編寫
ViewModel
和LiveData
單元測試(unit tests)。
建立單元測試目錄
單元測試一律位於 test
目錄中:
- 從「Android」切換至「Project」。
- 按一下第一個下拉式選單,然後依序點選「app」->「src」。
- 用滑鼠右鍵按一下「src」,然後選取「New」->「Directory」。

- 輸入並選取
test/java
。

- 現在會在專案結構中看到「src」->「test」目錄。

- 用滑鼠右鍵按一下「java」目錄,然後依序選取「New」->「Package」。

- 在視窗中輸入
com.example.cupcake
,然後按下 Return 鍵。

- 最後,建立名為
ViewModelTests.kt
的新 class。用滑鼠右鍵按一下com.example.cupcake
,然後依序選取「New」->「Kotlin Class/File」。

- 在出現的視窗中輸入
ViewModelTests
,然後從下拉式選單中選取「Class」。

新增必要的 dependencies
將下列 dependencies 新增至專案:
1 | testImplementation 'junit:junit:4.+' |
接著同步(sync)專案。
編寫和執行 ViewModel 測試
編寫單元測試
讓我們先從簡單的測試開始。當我們在裝置(device)或模擬器(emulator)上與 app 互動時,首先會選取杯子蛋糕(cupcakes)的數量(quantity)。因此,我們會在 OrderViewModel
中測試 setQuantity()
方法,並檢查 quantity
LiveData
物件的 value。
我們要測試的 quantity
變數是 LiveData
instance。測試 LiveData
物件需要執行額外步驟,因此,我們所新增的 dependency 就能派上用場。只要 value 有變更,我們就會使用 LiveData
更新 UI。UI 於「主執行緒(main thread)」上運作。如果您不熟悉執行緒(threading)和並行(concurrency)的概念,別擔心,我們會在其他程式碼研究室中深入介紹。
就 Android 應用程式而言,請暫時將主執行緒(main thread)視為 UI 執行緒(thread)。向使用者顯示 UI 的程式碼會在此執行緒(thread)上運作。除非另有指定,否則單元測試(unit test)會假設所有項目皆在主執行緒(main thread)上運作。不過,由於 LiveData
物件無法存取主執行緒(main thread),因此必須明確指出 LiveData
物件不得呼叫主執行緒(main thread)。
- 如要指定
LiveData
物件不得呼叫主執行緒(main thread),我們需在每次測試LiveData
物件時提供專用測試規則(test rule)。
1 |
|
- 現在可建立名為
quantity_twelve_cupcakes()
的 function。在 method 中,建立OrderViewModel
instance。
1 |
|
在本測試中,您需確認
OrderViewModel
中的quantity
物件在setQuantity
呼叫時已更新。但在呼叫任何 method 或處理OrderViewModel
中的任何資料前,請注意測試LiveData
物件的 value 時,必須先觀察(observed)物件,才能發出變更。方法很簡單,只要使用observeForever
method 即可。呼叫quantity
物件的observeForever
method 。這個 method 需要 lambda 運算式,但可以留空(empty)。然後呼叫
setQuantity()
method,將12
傳入做為參數。
1 | // 在 method 中,建立 OrderViewModel instance |
- 我們可以放心推論
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 |
|
執行單元測試
- 點選 Run Test 執行
ViewModelTests
。

- 查看測試結果,顯示 Tests passed 即代表測試通過。

執行測試!恭喜!您剛剛已編寫第一個 LiveData
單元測試(unit test),這是 Modern Android Development 的重要技能。這種方式並未測試到大部分的商業邏輯,因此我們要設計較為深入的測試。
計算訂單價格是 OrderViewModel
的主要功能之一。當我們選取杯子蛋糕數量,並選取取貨日期時,就會執行此功能。價格計算是以 private method 進行,因此我們的測試無法直接呼叫此 method。只有 OrderViewModel
中的其他 method 可以進行呼叫。這些是 public method,因此我們會呼叫此類 method 觸發價格計算作業,以便確認價格值是否符合預期。
最佳做法
選取杯子蛋糕數量及日期時,價格將會隨之更新。儘管兩者都應進行測試,但我們通常建議針對單一功能進行測試。因此,我們會針對各測試建立不同的方法:
- 在數量(quantity)更新時用於測試價格(price)的 function
- 在更新日期(date)時用於測試價格(price)的 function
我們不希望測試結果因為另一項測試失敗而失敗。
建立名為
price_twelve_cupcakes()
的方法,並將其做為測試加上註解(加上@Test
)。在方法中,建立
OrderViewModel
instance,並呼叫setQuantity()
方法,將 12 傳入做為參數。
1 | val viewModel = OrderViewModel() |
- 查看
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 | val price: LiveData<String> = Transformations.map(_price) { |
此範例是應在測試中觀察(observed)到 LiveData
的原因。系統會使用 Transformation
設定 price
的 value。基本上,此程式碼會使用我們指派給 price
的 value,並將其轉換成貨幣格式,使我們不必手動執行。但這個程式碼還有其他含意。轉換 LiveData
物件時,除非有必要進行呼叫,否則系統不會呼叫程式碼,而會將資源(resources)儲存到行動裝置(mobile device)。只有在觀察(observe)到物件變更時,系統才會呼叫程式碼。當然,此操作是在 app 中執行,但我們也需要為測試進行相同操作。
- 在測試方法中,請先新增下列程式碼,再設定數量(quantity):
1 | viewModel.price.observeForever {} |
測試程式碼的編寫方式如下:
1 |
|
如果現在進行測試,應可通過測試。
