iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 8
1
Mobile Development

從0開始,全方面自動化測試Android App系列 第 8

[Day 8] 單元測試中的非同步問題,listener及lambda

非同步呼叫

昨天分享了單元測試在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分是

  • requestDisplayNameByListener 利用listener callback方式拿資料
  • requestDisplayNameByLambda 利用lambda拿資料

兩個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如下:

  • getDisplayNameByListener 讓Activity呼叫並透過listener接收data再回傳給IView介面的receivedUserName。
  • getDisplayNameByLambda 讓Activity呼叫並透過lambda接收data再回傳給IView介面的receivedUserName。
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

我們先來看listener callback部份。這邊mockk提供了一個叫CapturingSlot的mock方法來幫助我們mock listener class,我們只要把listener所用interface capture起來就可以直接控制回呼callback的值,然後在answer的區塊用slot.captured就可以抓到原本listener中的callback function然後指定任何回傳值是不是很方便,也就是你現在可以在單元測試中仿造任何server request透過這個Listener所得到的回傳值。

Lambda

在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來解決應該對大家的測試會有一定的幫助。


上一篇
[Day 7] 解決常見的單元測試難題 - Static
下一篇
[Day 9] 關於mockk的其它用法
系列文
從0開始,全方面自動化測試Android App30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言