# Unit Test - 測試Room database
---
* 延續整合測試 - [Unit Test - 測試Android Components](https://hackmd.io/pKa8Zt-7TdunWGdsn9UK8A?view)
## 這篇使用Room Database,並用整合測試來驗證Dao是否有正確執行
## 開始吧
### 情境
* 我們有一個資料庫,裡面包含一個資料表
* 資料表有幾個欄位,儲存相關的資料
* 有一個Dao定義一些SQL語法-在這例子使用 **新增、刪除**
### Gralde依賴
* 加入Room
* 一些常用的測試
```kotlin=
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
dependencies {
// Room
implementation "androidx.room:room-runtime:2.3.0"
kapt "androidx.room:room-compiler:2.3.0"
// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:2.3.0"
// Local Unit Tests
implementation "androidx.test:core:1.3.0"
testImplementation "junit:junit:4.13.2"
testImplementation "org.hamcrest:hamcrest-all:1.3"
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.robolectric:robolectric:4.3.1"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2"
testImplementation "com.google.truth:truth:1.0.1"
testImplementation "org.mockito:mockito-core:2.23.0"
// Instrumented Unit Tests
androidTestImplementation "junit:junit:4.13.2"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.12.1"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2"
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation "com.google.truth:truth:1.0.1"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation "org.mockito:mockito-core:2.23.0"
}
```
### 建立資料庫需要的類別與介面
* data class ->定義我們需要的欄位名稱、類型
* Dao -> 操作資料庫的方法
* Database ->本體
#### data class
* 這邊例子使用購物清單
```kotlin=
@Entity(tableName = "shopping_items")
data class ShoppingItem(
var name: String,
var amount: Int,
var price: Float,
var imageUrl: String,
@PrimaryKey(autoGenerate = true)
val id: Int? = null
)
```
#### Dao
* 使用新增、刪除
* 兩個函數返回LiveData,不需要使用suspend fun
```kotlin=
@Dao
interface ShoppingDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertShoppingItem(shoppingItem: ShoppingItem)
@Delete
suspend fun deleteShoppingItem(shoppingItem: ShoppingItem)
@Query("SELECT * FROM shopping_items")
fun observeAllShoppingItems(): LiveData<List<ShoppingItem>>
@Query("SELECT SUM(price * amount) FROM shopping_items")
fun observeTotalPrice(): LiveData<Float>
}
```
#### Database
```kotlin=
@Database(
entities = [ShoppingItem::class],
version = 1
)
abstract class ShoppingItemDatabase : RoomDatabase() {
abstract fun shoppingDao(): ShoppingDao
}
```

### 開始寫測試程式
* 因為是整合測試,所以要在androidTest這個路徑底下新增
* package盡量一致,也放在專案名稱/data/local
* 命名方式,依照要測試的檔案名稱加上Test,ShoppingDaoTest
### 程式碼簡單說明
#### 先完成初始化
```kotlin=
@RunWith(AndroidJUnit4::class)
@SmallTest
class ShoppingDaoTest {
//定義資料庫與Dao
private lateinit var database: ShoppingItemDatabase
private lateinit var dao: ShoppingDao
//初始化資料庫的instance & Dao
@Before
fun setUp(){
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
ShoppingItemDatabase::class.java
).allowMainThreadQueries()
.build()
dao = database.shoppingDao()
}
//當資料庫操作完畢,要close
@After
fun tearDown(){
database.close()
}
}
```
1. @RunWith(AndroidJUnit4::class)是宣告這個測試程式要運行在這類別,不是JVM
2. @SmallTest是聲明這個測試它的範圍?耗時?,Google有一張表可以參考
[Test Sizes](https://testing.googleblog.com/2010/12/test-sizes.html)

#### 測試函數
```kotlin=
@Test
fun insertShoppingItem() = runBlockingTest {
val shoppIngItem = ShoppingItem("name", 1,1f,"url",1)
dao.insertShoppingItem(shoppIngItem)
val allShoppingItems = dao.observeAllShoppingItems().getOrAwaitValue() // return liveData
assertThat(allShoppingItems).contains(shoppIngItem)
}
```
1. 資料庫的操作,通常寫在IO執行緒,所以用runBlockingTest替代,它跟用runBlocking差別在於會略過像delay(1000)這種suspend fun,直接執行coroutine block本體程式碼
2. 
3. runBlockingTest會警告說要加入聲明,在測試類別加入@ExperimentalCoroutinesApi
4. 
5. 建立一筆資料並執行Datebase的寫入功能
6. 再透過Dao的方法讀取剛剛寫入的資料,因為這個方法返回LiveData,google有提供方法可以直接獲取到資料,在test的跟目錄創建一個類別,將下面的code寫進去就可以用了
7. 透過getOrAwaitValue()就可以拿到資料
8. 再比對查詢的資料是否有包含寫入的資料,來完成驗證
```kotlin=
package com.androiddevs.shoppinglisttestingyt
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
*
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}
```
#### 你還缺少規則
* 如果對剛剛的測試程式碼執行,它會跳出錯誤,說你的job尚未完成

* 必須新增規則
* 在執行一次就綠燈了~~
```kitlin=
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@SmallTest
class ShoppingDaoTest {
//這邊概念有點像是讓livedata立即notify change給他的observer
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var database: ShoppingItemDatabase
private lateinit var dao: ShoppingDao
}
```

#### 完成剩下的測試函數
* 刪除
* 計算購買項目總價錢
```kotlin=
@Test
fun deleteShoppingItem() = runBlockingTest {
val shoppIngItem = ShoppingItem("name", 1, 1f, "url", 1)
dao.insertShoppingItem(shoppIngItem) //寫入
dao.deleteShoppingItem(shoppIngItem) //刪除
val allShoppingItems = dao.observeAllShoppingItems().getOrAwaitValue() // return liveData
//比對寫入的資料是否不存在查詢的資料
assertThat(allShoppingItems).doesNotContain(shoppIngItem)
}
@Test
fun calculateTotalPrice() = runBlockingTest {
val shoppIngItem1 = ShoppingItem("name", 2, 1f, "url", 1)
val shoppIngItem2 = ShoppingItem("name", 3, 6.5f, "url", 2)
val shoppIngItem3 = ShoppingItem("name", 0, 100f, "url", 3)
dao.insertShoppingItem(shoppIngItem1)
dao.insertShoppingItem(shoppIngItem2)
dao.insertShoppingItem(shoppIngItem3)
//寫入三筆資料,比對價格是否一樣
val allShoppingPrice = dao.observeTotalPrice().getOrAwaitValue() // return liveData
assertThat(allShoppingPrice).isEqualTo(2 * 1f + 3 * 6.5f + 0 * 100f)
}
```
### Room的測試方法大概就如上述..再嘗試修改的方法
```kotlin=
@Test
fun updateShoppingItem() = runBlockingTest {
val shoppIngItem1 = ShoppingItem("name", 2, 1f, "url", 1)
val updateItem1 = ShoppingItem("banana", 2, 1f, "url", 1)
dao.insertShoppingItem(shoppIngItem1)
dao.updateShoppingItem(updateItem1)
val afterUpdate = dao.observeAllShoppingItems().getOrAwaitValue()
assertThat(afterUpdate).isEqualTo(listOf(ShoppingItem("banana", 2, 1f, "url", 1)))
}
```
參考資料
[Philipp Lackner channel](https://www.youtube.com/watch?v=xGbr9LOSbC0)
###### tags: `test` `Unit Test` `kotlin` `Android`