iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 19
0
自我挑戰組

Android Architecture 及 Unit Test系列 第 19

[Day 19] Test:Part 1 Datebase Dao

  • 分享至 

  • xImage
  •  

接下來要進行一系列的測試,會涵蓋 Database 到 Activity/Fragment 等部分,下面就先開始進行 DB 的單元測試。

Gradle

首先需要 import 幾個比較重要的 library ,例如 AndroidX Test 、 MockK、Google Truth 、Erpresso 、Robolectric,同時也要為 Room 、 Dagger 加入他們專屬的 Testing compiler library ,這裏就不一一細講了,具體 import 的 library 可以看看 Gist

Before Testing

在寫測試之前,需要知道在 Android 裡的測試可以大致分成兩個 folder:

  • androidTest:適合測試需要 Android API 的測試,例如 UI Test 、Database Test 等會用到 Android 相關內容的測試。
  • test:基於 JUnit 上,負責 Java/Kotlin 相關的測試,適合測試一些邏輯相關的內容。

以今天的主題為例,就會選擇在 androidTest 裡完成。

...以上是大部分教學所提到的內容,但我個人習慣在寫測試前先調整一下存放 Test 的位置。

由於現在在寫測試時常常發生既會使用 Android API 又要用 JUnit 寫一些單元測試,又或者一些 Test Util 可能在上面兩類測試都會用到,遇到這些情況把 class 放在哪裡都不太好,所以我另外會再開一個 share folder ,用來放這些 class 。

首先 build.gradle 內宣告創建一個跨兩個測試的資料夾 sharedTest :

    android {
        sourceSets {
            String sharedTestDir = 'src/sharedTest/java'
            test {
                java.srcDir sharedTestDir
            }
            androidTest {
                java.srcDir sharedTestDir
            }
        }
    }

接著因為我的專案名稱是 com.ininmm.todpapp ,所以建立以下資料夾:src/sharedTest/java/com/ininmm/todoapp

回到剛剛話題, Database 的測試會用到 Android API 及一些 JUnit 的東西,所以待會的測試就可以寫在這裡了。

Getting Start

今天要對 Room 的 Dao 進行測試,前面提到了 Dao 有使用 Android API ,所以讓我們先在 sharedTest 下建立一個 Test Class TasksDaoTest

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@SmallTest
class TasksDaoTest {

}

@Dao
interface TasksDao {

    @Query("SELECT * FROM tasks")
    suspend fun getTasks(): List<Task>

    @Query("SELECT * FROM tasks WHERE entryid = :taskId")
    suspend fun getTaskById(taskId: String): Task?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertTask(task: Task): Long

    @Update
    suspend fun updateTask(task: Task): Int

    @Query("UPDATE Tasks SET completed = :completed WHERE entryid = :taskId")
    suspend fun updateComplete(taskId: String, completed: Boolean)

    @Query("DELETE FROM Tasks WHERE entryid = :taskId")
    suspend fun deleteTaskById(taskId: String): Int

    @Query("DELETE FROM Tasks")
    suspend fun deleteTasks()

    @Query("DELETE FROM Tasks WHERE completed = 1")
    suspend fun deleteCompletedTasks(): Int
}

因為我們在 Room 中有使用其 coroutines 的部分,所以標上 ExperimentalCoroutinesApi 表明 coroutines 的某些 API 未來可能會變動。

使用 RunWith 告知 JUnit 接下來要執行的測試要使用那個 class 執行,而 AndroidJUnit4 正式 Android 用來在 Android 環境中執行 JUnit 的 runner 。

標上 SmallTest 則是 Android 告訴測試這是一個輕量、執行時間短的 Test ,有以下幾種 annotation 可供選擇:

  • SmallTest:與系統隔離執行,執行時間較短
  • MediumTest:集成了多個元件,並可以在模擬器或者真機上執行
  • LargeTest:可以執行UI流程的測試工作,確保APP按照預期在模擬器或實際裝置上工作

接著設置在每次開始測試前,先取得 Room 的 DataBase ,這邊可以使用 in-memory database ,可以在不影響 app 裡的資料下進行 DB 的測試,當然在完成測試後,也需要把 Database 關閉。

另外由於 Architecture Components 在執行操作時會自動切換到他自己的 background executor 執行,需要再加上 InstantTaskExecutorRule ,這樣執行測試時可以把 Architecture Components 的 thread 切換成一個同步的 executor 。

完成的程式碼如下:

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@SmallTest
class TasksDaoTest {

    private lateinit var roomDatabase: ToDoRoomDatabase
    
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()
    
    @Before
    fun setup() {
        roomDatabase = Room.inMemoryDatabaseBuilder(
            getApplicationContext(),
            ToDoRoomDatabase::class.java
        ).allowMainThreadQueries().build()
    }
    
    @After
    fun dropdown() {
        roomDatabase.close()
    }
}

再來看看寫測試時的方法命名,通常測試方法的名稱有一定的規範,共同目標旨在說明這個測試要測什麼,預期結果是什麼

假設要測試 TasksDao.getTaskById() 這個方法,可以建立一個 function 叫做 when_insert_task_then_get_by_id_success() ,表示這個測試的前提是先 insertTask() ,並且測試 getTaskById() 驗證結果成功。

關於命名的方式有許多流派,更詳細的資訊可以查看這裡,可以自己選擇一個喜歡的命名方式,反正只要能夠描述清楚測試的目標及結果即可。

最後具體的寫法,我們先寫一個基於 BBD 的測試,基本上只需要遵守 Given-When-Then 的原則就好。

  • Given:指定測試的預設,即先決條件或是必要的初始化
  • When:所要執行的測試
  • Then:得到預期的結果,我們會在這裡進行驗證

接下來看看完成的測試內容:

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@SmallTest
class TasksDaoTest {

    private lateinit var roomDatabase: ToDoRoomDatabase
    
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()
    
    @Before
    fun setup() {
        roomDatabase = Room.inMemoryDatabaseBuilder(
            getApplicationContext(),
            ToDoRoomDatabase::class.java
        ).allowMainThreadQueries().build()
    }
    
    @Test
    fun when_insert_task_then_get_by_id_success() = runBlockingTest {
        // Given:進行初始化,先往 Database 塞入一個 Task
        val task = Task("title", "description")
        roomDatabase.tasksDao().insertTask(task)

        // When:執行 getTaskById() 後獲得結果
        val loaded = roomDatabase.tasksDao().getTaskById(task.id)

        // Then:對結果進行驗證
        assertThat<Task>(loaded as Task, notNullValue())
        assertThat(loaded.id, `is`(task.id))
        assertThat(loaded.title, `is`(task.title))
        assertThat(loaded.description, `is`(task.description))
        assertThat(loaded.isCompleted, `is`(task.isCompleted))
    }
    
    @After
    fun dropdown() {
        roomDatabase.close()
    }
}

篇幅有限無法在這裡貼上全部的測試程式,有興趣可以看看這邊,但我想今天已經完成第一個簡單的測試了。


上一篇
[Day 18] DataBinding
下一篇
[Day 20] Test:Part 2 DataSource
系列文
Android Architecture 及 Unit Test30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言