---
# System prepended metadata

title: Unit Test - 使用hilt測試4 - testing ViewModel
tags: [Unit Test, hilt, test, kotlin]

---

# 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`