# Unit Test - 使用hilt測試4 - testing ViewModel --- ## 前置作業完成,開始寫viewModel測試 ## 開始吧 ### 情境 ### 建立測試類別 * 前面有提過,使用假的repository模擬真正的api或者db操作(速度快) * 所以不會使用到相關元件,因此我們是建立unit test,不是整合測試 ![](https://i.imgur.com/mCSNY70.png) ### 開始寫測試程式吧 * 那viewModel需要給它repository的參數 * 這個參數就用之前寫的FakeRepository ```kotlin= class ShoppingViewModelTest { private lateinit var viewModel: ShoppingViewModel @Before fun setup(){ //如果參數給它DefaultRepository,就需要再給Dao & API等參數會更複雜 viewModel = ShoppingViewModel(FakeRepository()) } } ``` * 編寫測試函數 * 測試條件: 當輸入的欄位為empty,Resource這類別會return error * 在Constants增加2個條件 ```kotlin= object Constants { const val DATABASE_NAME = "shopping_db" const val BASE_URL = "https://pixabay.com" const val MAX_NAME_LENGTH = 20 //名稱長度限制 const val MAX_PRICE_LENGTH = 10 //金額長度限制 } ``` ```kotlin= /** * 新增一筆資料,數量為empty * */ @Test fun `insert shopping item with empty field, returns error`() { viewModel.insertShoppingItem("name", "", "5.0") val value = viewModel.insertShoppingItemStatus.getOrAwaitValueTest() //透過getContentIfNotHandled取值,若被處理過,就會拿到null assertThat(value.getContentIfNotHandled()?.status).isEqualTo(Status.ERROR) } @Test fun `insert shopping item with too long name, returns error`() { val string = buildString { for (i in 1..Constants.MAX_NAME_LENGTH + 1) { append(1) } } viewModel.insertShoppingItem(string, "8", "5.0") val value = viewModel.insertShoppingItemStatus.getOrAwaitValueTest() //透過getContentIfNotHandled取值,若被處理過,就會拿到null assertThat(value.getContentIfNotHandled()?.status).isEqualTo(Status.ERROR) } @Test fun `insert shopping item with too long price, returns error`() { val string = buildString { for (i in 1..Constants.MAX_PRICE_LENGTH + 1) { append(1) } } viewModel.insertShoppingItem("name", "8", string) val value = viewModel.insertShoppingItemStatus.getOrAwaitValueTest() //透過getContentIfNotHandled取值,若被處理過,就會拿到null assertThat(value.getContentIfNotHandled()?.status).isEqualTo(Status.ERROR) } @Test fun `insert shopping item with too high amount, returns error`() { //這邊數量直接定義一個超過Int型態所容許的值 /** * Kotlin 中屬於整數型態的有: Byte 範圍為: -128 ~ 127 Short 範圍為: -32768 ~ 32767 Int 範圍為: -2147483648 ~ 2147483647 Long 範圍為: -9223372036854775808 ~ 9223372036854775807 */ viewModel.insertShoppingItem("name", "999999999999999999999999", "5.0") val value = viewModel.insertShoppingItemStatus.getOrAwaitValueTest() //透過getContentIfNotHandled取值,若被處理過,就會拿到null assertThat(value.getContentIfNotHandled()?.status).isEqualTo(Status.ERROR) } //當輸入符合條件的參數,Status會得到SUCCESS @Test fun `insert shopping item with valid input , returns success`() { viewModel.insertShoppingItem("name", "10", "5.0") val value = viewModel.insertShoppingItemStatus.getOrAwaitValueTest() //透過getContentIfNotHandled取值,若被處理過,就會拿到null assertThat(value.getContentIfNotHandled()?.status).isEqualTo(Status.SUCCESS) } ``` ### 執行ShoppingViewModelTest測試 * 你會發現跳出這個錯誤視窗 * 這裡有說明: [Blog](https://blog.jetbrains.com/idea/2017/10/intellij-idea-2017-3-eap-configurable-command-line-shortener-and-more/) **Configurable command line shortener** When the classpath gets too long, or you have many VM arguments, the program cannot be launched. The reason is that most operating systems have a command line length limitation. In such cases IntelliJ IDEA will try to shorten the classpath. ![](https://i.imgur.com/iqL8SaI.png) * 點選Default * Shorten command line選 JAR ![](https://i.imgur.com/Eia0UxL.png) * 再執行一次 java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. ![](https://i.imgur.com/0SrJZlC.png) * 要加入Rule ```kotlin= //這個rule有點像是把非同步變成同步? /** *A JUnit Test Rule that swaps the background executor used by the Architecture Components with a different one which executes each task synchronously. You can use this rule for your host side tests that use Architecture Components. **/ @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() ``` * 再次執行 * 可以看到亮紅燈,並顯示LiveData value was never set. * 這裡就可以對比到TDD的第1跟第2步驟了 ![](https://i.imgur.com/AHwAzc9.png) ### 根據測試條件,實作viewModel的function ```kotlin= fun insertShoppingItem(name: String, amountString: String, priceString: String) { if (name.isEmpty() || amountString.isEmpty() || priceString.isEmpty()) { _insertShoppingItemStatus.postValue(Event(Resource.error("輸入參數不能是empty", null))) return } if (name.length > Constants.MAX_NAME_LENGTH) { _insertShoppingItemStatus.postValue( Event( Resource.error( "名稱長度不得超過${Constants.MAX_NAME_LENGTH}", null ) ) ) return } if (priceString.length > Constants.MAX_PRICE_LENGTH) { _insertShoppingItemStatus.postValue( Event( Resource.error( "價格長度不得超過${Constants.MAX_PRICE_LENGTH}", null ) ) ) return } val amount = try { amountString.toInt() } catch (e: Exception) { _insertShoppingItemStatus.postValue(Event(Resource.error("請輸入合法的數量", null))) return } val shoppingItem = ShoppingItem(name, amount, priceString.toFloat(), _curImageUrl.value ?: "") insertShoppingItemIntoDb(shoppingItem) setCurImageUrl("") //這個可以寫測試 _insertShoppingItemStatus.postValue(Event(Resource.success(shoppingItem))) } fun searchForImage(imageQuery: String) { if (imageQuery.isEmpty()) { return } //在unit test中,使用 // value: 可以立刻觸發observer //.postValue: 若在短時間內觸發多次postValue,它只會在最後一次觸發observer //在unit test,要確保observer有確實的執行.故選擇value _images.value = Event(Resource.loading(null)) viewModelScope.launch { val response = repository.searchForImage(imageQuery) _images.value = Event(response) } } ``` ### 再次執行測試 * 會發現明明亮綠燈,可是還是有錯誤 ![](https://i.imgur.com/A9x3Mgw.png) * 原因是我們在viewModel有呼叫insertShoppingItemIntoDb() * 它是一個suspend call,它用Main Dispatcher,而在test並沒有main Dispatcher * 而main Dispatcher依賴於main looper,而looper只存在於真正的app,unit test run在JVM,所以無法存取 * 要解決這個問題,就是要另外定義一個junit rule ### 建立MainCoroutineRule ```kotlin= //在test的根目錄建立 @ExperimentalCoroutinesApi class MainCoroutineRule( private val dispatcher: CoroutineDispatcher = TestCoroutineDispatcher() ) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher){ override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(dispatcher) } override fun finished(description: Description?) { super.finished(description) cleanupTestCoroutines() Dispatchers.resetMain() } } ``` * 並在ShoppingViewModelTest新增剛剛建立的Rule ```kotlin= @ExperimentalCoroutinesApi class ShoppingViewModelTest { @get:Rule var mainCoroutineRule = MainCoroutineRule() ``` * 再次執行一次.綠燈pass ![](https://i.imgur.com/5o8cxlN.png) ###### tags: `test` `Unit Test` `hilt` `kotlin`