今天打算完成 Repository 的測試,那就直接先開一個 TasksRepositoryTest
吧,一樣先初始化要測試的 TasksRepository
,不過今天有一點不一樣了。
TasksRepository
需要注入兩個 DataSource ,如果要建立這兩個 DataSource 又要再建立其他的依賴物,這時就可以使用 Mocking Framework 來幫助我們建立兩個假的 DataSource ,再把他們放到 TasksRepository
裡,從而解決我們的問題。
一般大家比較常用的 Mocking 工具是 Mockito
,但是在 Kotlin 裡使用 Mockito 會遇到一些問題,為了這個問題去年我在 JCConf 上詢問一位 JetBrains 的工程師時,他推薦我在寫 Kotlin 的測試時可以使用專門為 kotlin 打造的 MockK ,因此這次我特地使用 MockK 實作看看。
今天只會大概提到 MockK 在這個測試裡的使用情境,想要知道更詳細的教學可以參考官網或是其他人的心得。
先來看看如何使用 MockK 建立兩個 DataSource :
@ExperimentalCoroutinesApi
class TasksRepositoryTest {
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
private lateinit var tasksLocalDataSource: TasksDataSource
private lateinit var tasksRemoteDataSource: TasksDataSource
private lateinit var tasksRepository: ITasksRepository
@Before
fun setup() {
tasksLocalDataSource = mockk()
tasksRemoteDataSource = mockk()
tasksRepository = TasksRepository(
tasksRemoteDataSource,
tasksLocalDataSource,
Dispatchers.Main
)
}
}
其實非常簡單,而且跟 Mockito 有一點相似,除此之外還有其他的 initial 方法,之後有機會也會提到。
還記得 TasksRepository 嗎?之前我寫了一個比較複雜的 getTasks
方法,如果想要為這個方法寫測試,要如何下手?
其實沒有十分困難,目標同樣是要測試 getTasks
方法的結果是否如預期所想的一樣,不需要在乎傳入的東西或是具體的執行流程。只是這次在 getTasks
裡有使用剛剛 mock 的兩個 DataSource ,這樣要如何使用這兩個假的 instance 呢?
事實上 Mock 可以假裝真的有 "調用" 這個方法,甚至還可以假裝這個方法被調用,而且還有回傳值。
這邊就可以利用 MockK 幫我們假裝 DataSource 內的方法有被調用或是有回傳。
先來看看 getTasks
方法,追蹤一下 code 可以看到我們分別調用了 DataSource 裡的四個方法:
這裏我們可以假裝這些方法有被調用或是有回傳值,這樣只要我們接住 getTasks
的結果並加以驗證,就可以完成這個測試了。
假如以 mock tasksRemoteDataSource.getTasks()
為例, MockK 提供了 every
方法,與 Mockito 的 when().thenReturn()
相似,目的都是指定呼叫目標方法 return 時一律都回傳某個值。
實作後會寫成這樣:
@Test
fun getTasksEmptyRepositoryThenReturnRemoteData() = runBlockingTest {
val tasksRemote = listOf(
Task("Title3", "Description3"),
Task("Title4", "Description4")
).sortedBy { it.id }
coEvery { tasksRemoteDataSource.getTasks() } returns Success(tasksRemote)
}
這裏由於指定的方法使用了 Coroutines , MockK 又提供一個 coEvery
方法針對 Coroutines 處理;接著使用 returns
指定現在呼叫 tasksRemoteDataSource.getTasks()
一律回傳 Result.Success(tasksRemote)
。
另外像是 tasksLocalDataSource.deleteAllTasks
這種沒有回傳值的方法可以寫成以下:
coEvery { tasksLocalDataSource.deleteAllTasks() } just Runs
coEvery { tasksLocalDataSource.saveTask(any()) } just Runs
just Runs
表示遇到這個方法時可以直接執行 ,而 saveTask(any())
則表示不在乎傳入的是什麼東西。
最後再進行驗證即可。
另外在測試一些沒有回傳值的方法時,有一種手法是驗證裡面的某些關鍵方法有沒有被調用, MockK 也提供 verify
方法解決,例如 tasksRepository.saveTask()
本身沒有回傳值,可以透過驗證 DataSource 的 saveTask())
是否有被調用來當作測試結果:
@Test
fun saveTaskThenSaveToCacheLocalAndRemote() = runBlockingTest {
// Given
val newTask = Task("Title new", "Description new")
coEvery { tasksLocalDataSource.saveTask(any()) } just Runs
coEvery { tasksRemoteDataSource.saveTask(any()) } just Runs
// When
tasksRepository.saveTask(newTask)
// Then
coVerify { tasksLocalDataSource.saveTask(any()) }
coVerify { tasksRemoteDataSource.saveTask(any()) }
}
一樣由於有使用 Coroutines 所以改成用 MockK 的 coVerify
驗證。
今天大概到這邊,完整的 TasksRepositoryTest 可以看後面的連結。