# Unit Test - 使用hilt測試4 - testing ViewModel
---
## 前置作業完成,開始寫viewModel測試
## 開始吧
### 情境
### 建立測試類別
* 前面有提過,使用假的repository模擬真正的api或者db操作(速度快)
* 所以不會使用到相關元件,因此我們是建立unit test,不是整合測試

### 開始寫測試程式吧
* 那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.

* 點選Default
* Shorten command line選 JAR

* 再執行一次
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked.

* 要加入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步驟了

### 根據測試條件,實作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)
}
}
```
### 再次執行測試
* 會發現明明亮綠燈,可是還是有錯誤

* 原因是我們在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

###### tags: `test` `Unit Test` `hilt` `kotlin`