iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 26
0

介紹完 unit test 之後,大家應該有發現幾乎所有的情境都可以由測試 3A(Arrange、Act、Assert)法則來驗證我們的程式是否正確。

可是如果函式內部用到的外部 dependency 很難在測試環境初始化,例如說 Android SDK 裡的物件,又或者是沒有回傳值,我們必須去檢查是否有正常呼叫到該呼叫的其他元件,這時候就會讓測試變得困難許多,這時候我們就會需要其他工具了。

我們先修改一下上一章節的例子,讓 AuthManager 依賴外部的 ILoginService 來執行登入動作如下:

class AuthManager(private val loginService: ILoginService) {

    fun login(account: String, password: String): Boolean {
        return loginService.login(account, password)
    }
}

interface ILoginService {
    fun login(account: String, password: String): Boolean
}

class LoginService : ILoginService {
    override fun login(account: String, password: String): Boolean {
        //....
        return true
    }
}

所以實際執行可以這樣呼叫:

val loginService = LoginService()
val authManager = AuthManager(loginService)
authManager.login("123456", "12345678")

而測試函式可以這樣寫:

@Test
fun login() {
    val loginService = LoginService()
    val authManager = AuthManager(loginService)
    val result = authManager.login("123456", "12345678")
    Assert.assertEquals(true, result)
}

但如果今天我們的 LoginService 無法在測試環境初始化,我們要怎麼辦呢?

這時候就是 Mockito 派上用場的時候了!

Mockito

一樣要先宣告 dependency 在 build.gradle

dependencies {
    testImplementation 'org.mockito:mockito-core:3.1.0'
}

Mock

Mockito 提供了簡單的 mock 函式,只要把目標 class 丟進去,就可以得到一個假的目標物件。

val loginService = Mockito.mock(ILoginService::class.java)

有了這個假物件後,我們可以使用 whenverify 來做一些互動的操作與檢查:

When

我們的 mock 物件並含任何邏輯,Mockito 提供給我們 when 這個函式來指定特定函式我們想要回傳的值。
比如說我們可以設定當帳號密碼都符合長度時回傳 true,而密碼為空的情況就是 false

Mockito.`when`(loginService.login("123456", "12345678"))
    .thenReturn(true)

Mockito.`when`(loginService.login("123456" ""))
    .thenReturn(false)

`when` 的出現是因為在 kotlin 裡,when 已經變成個關鍵字了。

Verify

verify 是我們用來檢查特定函式是否有被特定的方式呼叫。
比如說,我們想要檢查有沒有人呼叫 loginService.login("123456", "12345678") 可以這樣寫:

Mockito.verify(loginService).login("123456", "12345678")

如果我們不在意參數的內容:

Mockito.verify(loginService).login(Mockito.anyString(), Mockito.anyString())

如果我們希望檢查 login 有沒有被呼叫三次:

Mockito.verify(loginService, Mockito.times(3))
    .login(Mockito.anyString(), Mockito.anyString())

綜合 whenverify,我們的測試函式就會變成這樣:

@Test
fun login() {
    val loginService = Mockito.mock(ILoginService::class.java)
    val authManager = AuthManager(loginService)
    Mockito.`when`(loginService.login("123456", "123456578"))
        .thenReturn(true)
    val result = authManager.login("123456", "123456578")
    Mockito.verify(loginService).login("123456", "123456578")
    Assert.assertEquals(true, result)
}

Annotation

我們可以使用 annotation 進一步讓我們的程式碼變得更簡潔,mock 物件可以透過 @Mock 標示來省去直接呼叫 Mockito.mock 的麻煩,記得 class 也要標示 @RunWith(MockitoJUnitRunner::class) 如下:

@RunWith(MockitoJUnitRunner::class)
class AuthManagerTest {

    @Mock
    lateinit var loginService: ILoginService
}

看起來很完美,我們試著把 ILoginService interface 改成 LoginService class 看看會發生什麼事情?

很遺憾的,你應該會得到以下結果:

org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class com.example.androidtestsample.LoginService
Mockito cannot mock/spy because :
 - final class

如果我們一直遵守著 SOLID 的 dependency inversion principle,任何 dependency 應該依賴於 interface 而不是 class,或許問題不大,但我們還是有可能遇到第三方 library 的依賴好死不死就是寫著 final,這時候該怎麼辦呢?

這時候可以借助另一個 library - PowerMock,來幫我們處理 final、static、private 等無法測試的情形囉!
有興趣的讀者請自行參考:
https://github.com/powermock/powermock

以上就是今天的全部內容了,希望對大家有幫助!!
也歡迎參考 Android 十全大補的測試三部曲的其他文章:

參考資料:
https://github.com/mockito/mockito


上一篇
[Android 十全大補] Unit Test
下一篇
[Android 十全大補] Espresso
系列文
Android 十全大補30

尚未有邦友留言

立即登入留言