介紹完 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 派上用場的時候了!
一樣要先宣告 dependency 在 build.gradle
:
dependencies {
testImplementation 'org.mockito:mockito-core:3.1.0'
}
Mockito 提供了簡單的 mock
函式,只要把目標 class 丟進去,就可以得到一個假的目標物件。
val loginService = Mockito.mock(ILoginService::class.java)
有了這個假物件後,我們可以使用 when
跟 verify
來做一些互動的操作與檢查:
我們的 mock 物件並含任何邏輯,Mockito 提供給我們 when
這個函式來指定特定函式我們想要回傳的值。
比如說我們可以設定當帳號密碼都符合長度時回傳 true
,而密碼為空的情況就是 false
:
Mockito.`when`(loginService.login("123456", "12345678"))
.thenReturn(true)
Mockito.`when`(loginService.login("123456" ""))
.thenReturn(false)
`when`
的出現是因為在 kotlin 裡,when 已經變成個關鍵字了。
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())
綜合 when
跟 verify
,我們的測試函式就會變成這樣:
@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 進一步讓我們的程式碼變得更簡潔,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 十全大補已經正式出書上架囉!
有興趣的讀者歡迎參考:
https://www.tenlong.com.tw/products/9789864345786