iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 9
2
Mobile Development

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

[Day 9] 關於mockk的其它用法

今天要介紹的是mockk的其它用法,為什麼我要對mockk介紹這麼多篇?因為mockk不僅僅是在單元測試的時候會用到,在之後要介紹的integration test整合測試的部份也會用到,只要你的測試有用到mock仿造的地方一定都需要這些mocking framework的協助。對它越熟悉的話你的單元測試可應用的場景就會越廣泛。

Relaxed mock

在寫單元測試的時候,很容易會遇到一個問題,我們在被測試程式中有許多需要被mock的物件,但是對於一些mock物件雖然它是這個被測程試中的必須品,但它對於測試結果不重要,所以我不想去花時間在寫測試時去定義它的行為,但是你把一個物件mock後它又失去它原本的行為使得不得不去定義它,那該怎麼做才能減輕我們的負擔?

我們用[Day 8]講到的範例來延伸,我們在DemoPresenter裡面必須注入IView這個介面,而我們在verify的時候需要執行IView裡的receivedUserName(),因為我們mock IView的原故,所以我們必須去定義receivedUserName()被呼叫的時候要做什麼事,但是寫這個定義很無聊,因為它本來就該被執行,但在mock的架構下又不得不寫,那我們要怎麼省略這件事呢?只要利用relaxed的parameter就可以呼叫mockk幫我們做這件事。如範例中的mockk<IView>(relaxed = true),這時候mockk在呼叫所有關於IView的function都會幫我們做如同every區塊的宣告,也會做一個預設的回傳。如果你有其中一個function需要自己定義的話,你只要針對那個function去寫every宣告而不用管其它只需要bypass的function。

這個在你有一個複雜的mock class時候非常好用,假設IView有100個functions需要被mock,但這時你只想要更改receivedUserName()的回傳邏輯,只要把IView宣告成relaxed = true那就不用管其它99個function的宣告了。

@Test
    fun testRelaxedMock() {
        val requestManager = mockk<RequestManager>()
        //val view = mockk<IView>() 
        //本來這樣寫,改成下面加relaxed = true的寫法
        val view = mockk<IView>(relaxed = true)
        val presenter = DemoPresenter(requestManager, view)
        val slot = CapturingSlot<DataReceivedListener>()

        //every { view.receivedUserName(any()) } just Runs
        //本來規定要寫這行現在不用寫了,是不是很方便
        
        every {
            requestManager.requestDisplayNameByListener(any(), capture(slot))
        }.answers {
            slot.captured.onReceived(UserData("", ""))
        }

        presenter.getDisplayNameByListener()

        verify {
            view.receivedUserName(any())
        }
    }

relaxedUnitFun

順便一提,mockk還提供了另一個參數是relaxedUnitFun來取代relaxed,這是當你只需要把在Kotlin中回傳Unit的function請mockk先幫我們定義,而沒有回傳Unit的其它function都要自行定義就可以用relaxedUnitFun來宣告,這是一種防呆的寫法,沒回傳值就不寫有回傳值就要自己定義,但是一般來說用relaxed就可以了。像範例中的script也可以改成mockk<IView>(relaxedUnitFun = true)。

Constructor mock

有的時候我們用了3rd party library後會呼叫到對方的constructor去產生一些物件,然後這些物件又會被library拿去做一些事不在我們的測試中,該怎麼把contructor mock起來而不要真的去使用這些物件呢?這裡我直接舉一個Android開發上的實際例子,假設我們的程式中用到了Android SDK中的WorkManager的排程管理類別,而WorkManager的寫法是要把你要執行的程序用繼承WorkRequest的類別包起來,最簡單的用法是用一個OneTimeWorkRequest.Builder()去建立WorkRequest,然後再給WorkManager去執行。
還不熟悉的人可以參考google guideline https://developer.android.com/topic/libraries/architecture/workmanager/basics

但我們在單元測試會遇到一個狀況就是我們只是要verify WorkerManager是否有執行WorkRequest,而不是真的要去跑WorkRequest的結果,我們的做法就是要給WorkManager一個假的WorkRequest,不要去做Production code裡的實際程序,好死不死Builder的寫法是在Constructor裡去實體化WorkReqeust物件,我們要給mock的WorkRequest就要在Constructor裡動手腳,這時就可以透過mockk的幫助去做這件事。

可以看到寫法很簡單用mockkConstructor()來通知mockk我要覆寫contructor然後在every區塊呼叫constructor的時候用anyConstructed<>就可以在單元測試中return mock物件。

    val workRequest = mockk<OneTimeWorkRequest>(relaxed = true)
    val workManager = mockk<WorkManager>(relaxed = true)
    
    mockkStatic(WorkManager::class)
    mockkConstructor(OneTimeWorkRequest.Builder::class)
    every { WorkManager.getInstance() }.returns(workManager)
    every { anyConstructed<OneTimeWorkRequest.Builder>().build()}
    .returns(workRequest)
    
    //不用真的去執行WorkRequest,給mock的WorkRequest物件去做呼叫驗證就好
    verify {
        workManager.enqueue(workRequest)
    }

Spy & Private function

在Application開發過程當中,不免會用到一些private function。但是單元測試的原則是不mock private function,為什麼呢?因為private function應該屬於public function流程的一部份,不是靠setter注入的相依物件,而外部物件也只能呼叫public function,我們在測試public function的時候就會測試到private function。所以private function的設計應該要小而目的單純,如果耦合太多物件就有bad smell壞味道出現。但有時候我們真的想mock private function給予不同的測試條件要怎麼做?

mockk提供了Spy的recordPrivateCalls參數給我們使用。假設我們有Util class,public function calculate是把private function的getRate拿來做計算。如果我們在單元測試中想要更改getRate的回傳值要怎麼做?

class Util {
    private fun getRate(): Int {
        return 5
    }

    fun calculate(value: Int): Int {
        return getRate() * value
    }
}

在下面的testSpyPrivateFunction()中,我們的被測試者是Util的public function calculate,裡面會抓取private function getRate()來做計算,我們如果想要mockk getRate()的話,必須把Util用Spyk來宣告,之前有提過Spy的概念,Spy會呼叫真實的物件讓你做部份的mock,跟mock物件後是內部全部為空有一點不一樣。因為我們要取得private function的實體,所以要靠Spy來達成。Spy物件利用mockk的方法是呼叫Spkk,然後我們想用private function也必須宣告成recordPrivateCalls = true。最後把想要更改的private function用括號型式["name"]映射出來就可以了。

@Test
    fun testSpyPrivateFunction() {
        val spyUtil = spyk<Util>(recordPrivateCalls = true)
        every { spyUtil["getRate"]() }.returns(10)
        val expected = spyUtil.calculate(100)
        assertEquals(1000, expected)
    }

這裡介紹了一些mockk其他的用法,因為時間關係其它更多的就靠各位自行研究了。


上一篇
[Day 8] 單元測試中的非同步問題,listener及lambda
下一篇
[Day 10] MVVM與單元測試
系列文
從0開始,全方面自動化測試Android App30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言