昨天分享了單元測試在Android上面會遇到的第一個難題靜態類別後,今天要講在Android做單元測試時候常會遇到的另一種狀況callback回呼機制,callback目前常見的做法有兩種,用anonymous class所做的listener或是利用functional programming設計的lambda回傳的機制。會這樣是因為在mobile application的應用上免不了的有用非同步(asynchronous)的處理方式去跟back-end做資料的取得。不論是自已設計的server或是3rd party service,一定都會用到非同步的方式來跟mobile client做溝通。在單元測試裡面我們並不希望真的去跟server連線拿資料,單元測試要做的是驗證我們mobile client程式的邏輯而不是去驗證server回傳資料的正確性(這在之後end to end test的章節會另做介紹),那在單元測試的寫作裡要怎麼做呢?我想這應該是除了static的問題外第二個常會遇到的問題。
這裡我會舉一個MVP中Presenter的例子來給大家看,讓大家更容易理解及使用。
假設我們有一個RequestManager負責跟Server拿資料,裡面有兩個function分是
兩個function呼叫後都會從server的response中拿到UserData("1", "Daniel Chen")的示意data,這裡資料如何不重要,因為上面有提過server的資料在單元測試裡不是被測項目,因為我們不是要測試server的回傳資料的對或錯。
//這個interface跟前面章節範例一樣,用來傳遞資料到Activity
interface IView {
fun receivedUserName(name: String)
}
//listener interface
interface DataReceivedListener {
fun onReceived(data: UserData)
}
//data model
data class UserData(val id: String, val displayName: String)
class RequestManager {
//使用listner來回呼responsed data
fun requestDisplayNameByListener(id: String, listener: DataReceivedListener) {
//we have some server calls
listener.onReceived(UserData("1", "Daniel Chen"))
}
//使用lambda來回呼responsed data
fun requestDisplayNameByLambda(id: String, lambda: (data: UserData) -> Unit) {
//we have some server calls
lambda(UserData("1", "Daniel Chen"))
}
}
接下來我們有一個DemoPresenter去呼叫RequestManager裡的兩個functions,我們在DemoPresenter的constructor裡注入了RequestManager跟IView的實體,而在DemoPresenter我們有兩個functions用來給Activity呼叫,你可以假設這裡有個Activity透過DemoPresenter去呼叫這兩個function,但這個章節重點不在MVP模式,所以省略Activity的實作部份。還不熟悉的人可以回到[Day 5]的MVP介紹章節開始看。兩個給Activity呼叫的function如下:
class DemoPresenter(private val requestManager: RequestManager, val view: IView) {
fun getDisplayNameByListener() {
requestManager.requestDisplayNameByListener("1", object : DataReceivedListener {
override fun onReceived(data: UserData) {
view.receivedUserName(data.displayName)
}
})
}
fun getDisplayNameByLambda() {
requestManager.requestDisplayNameByLambda("1") { data ->
view.receivedUserName(data.displayName)
}
}
}
接下來我們來看如何測試Listener的callback function和lambda function,首先我們一樣先確認我們的被測class是DemoPresenter所以我們先把有相依關係的RequestManager跟IView mock起來給presenter使用。然後最後要驗證是是view.receivedUserName有被呼叫到,因為我們的範例要呼叫兩次view.receivedUserName()所以用exactly = 2來做輔助驗證。
我們先來看listener callback部份。這邊mockk提供了一個叫CapturingSlot的mock方法來幫助我們mock listener class,我們只要把listener所用interface capture起來就可以直接控制回呼callback的值,然後在answer的區塊用slot.captured就可以抓到原本listener中的callback function然後指定任何回傳值是不是很方便,也就是你現在可以在單元測試中仿造任何server request透過這個Listener所得到的回傳值。
在Kotlin Lambda的部份,要測試它也不會很困難,一樣透過mockk的幫助,我們利用指定captureLambda()這個函式就可以mock在這個位罝被傳入的lambda function,跟Listener的做法很像,但我們在answer區塊必須指定全部的lambda type型態跟回傳值就可以了其實也不難。我們範例中的lambda type是(data: UserData) -> Unit,只要把它的型別宣告好就可以任意invoke我們要的回傳值,所以使用起來很方便。
@Test
fun testListenerAndLambda() {
val requestManager = mockk<RequestManager>()
val view = mockk<IView>()
val presenter = DemoPresenter(requestManager, view)
val result = UserData("", "")
//之前的章節有提到,IView已經被mock所以我們呼叫recievedUserName的行為要先定義
//這裡就只是回傳void就好,可參考之前的說明
every { view.receivedUserName(any()) } just Runs
//slot是用來假造interface
val slot = CapturingSlot<DataReceivedListener>()
//這裡設定呼叫到listener的interface有什麼行為
every {
requestManager.requestDisplayNameByListener(any(), capture(slot))
}.answers {
slot.captured.onReceived(result)
}
//第一個被測function
presenter.getDisplayNameByListener()
//這裡設定呼叫到lambda有什麼行為
every {
requestManager.requestDisplayNameByLambda(any(), captureLambda())
} .answers {
lambda<(data: UserData) -> Unit>().invoke(result)
}
//第二個被測function
presenter.getDisplayNameByLambda()
//驗證區塊 exactly是用來驗證區塊內被呼叫幾次
verify (exactly = 2) {
view.receivedUserName(any())
}
}
今天介紹常見的mock問題和如何用mockk來解決應該對大家的測試會有一定的幫助。