Kotlin 寫 Android 單元測試(三),Mockito mocking 框架的使用

Kotlin 寫 Android 單元測試系列:

Kotlin 寫 Android 單元測試(一),單元測試是什么以及為什么需要

Kotlin 寫 Android 單元測試(二),JUnit 4 測試框架和 kotlin.test 庫的使用

Kotlin 寫 Android 單元測試(三),Mockito mocking 框架的使用

Kotlin 寫 Android 單元測試(四),Robolectric 在 JVM 上測試安卓相關代碼

Junit 4 測試框架可以驗證有直接返回值的方法,但是對于沒有返回值的 void 方法應該如何測試呢?void 方法的輸出結果其實是調用了另外一個方法,所以需要驗證該方法是否有被調用,調用時參數是否正確。Mocking 框架可以驗證方法的調用,目前流行的 Mocking 框架有 Mockito、JMockit、EasyMock、PowerMock 等。我選擇的是 Mockito 框架,原因是:(1)Mockito 是 Java 中最流行的 mocking 框架;(2)Google 的 Google Sample 下的開源庫中使用也是 Mockito 框架。下面介紹 Mockito 框架一些概念和用法,以及 Kotlin 中 mockito-kotlin 庫的使用。

本文是基于 Mockito 2.13.0 版本,Android Studio 3.0 環境

1. Mockito 框架

Gradle 引入

testImplementation 'org.mockito:mockito-core:2.13.0'
// 如果需要 mock final 類或方法的話,還要引入 mockito-inline 依賴
testImplementation 'org.mockito:mockito-inline:2.13.0'

先看下 Mockito 的一個簡單的例子(選自 Mockito 文檔):

 //mock creation
 //we can not use MutableList<String>::class.java as Class type
 val mockedList = mock(mutableListOf<String>().javaClass)

 //using mock object
 mockedList.add("one")
 mockedList.clear()

 //verification
 verify(mockedList).add("one")
 verify(mockedList).clear()

上面例子中可以看出,使用 Mockito 很容易驗證 mock 對象的方法調用,注意這里的限制是只能驗證 mock 對象的方法調用。

1.1 mock 和 spy

創建 mock 對象是 Mockito 框架生效的基礎,有兩種方式 mockspymock 對象的屬性和方法都是默認的,例如返回 null、默認原始數據類型值(0 對于 int/Integer)或者空的集合,簡單來說只有類的空殼子。而spy 對象的方法是真實的方法,不過會額外記錄方法調用信息,所以也可以驗證方法調用。

val mockedList = mock(mutableListOf<String>().javaClass)
val spyList = spy(mutableListOf<String>())

// mock object methods actually do nothing
mockedList.add("one")

// spy object call *real* methods
spyList.add("one")

Mockito 還提供了 @Mock 等注解來簡化創建 mock 對象的工作

class CalculatorTest {
    @Mock
    lateinit var calculator: Calculator

    @Spy
    lateinit var dataBase: Database

    @Spy
    var record = Record("Calculator")

    @Before
    fun setup() {
        // 必須要調用這行代碼初始化 Mock
        MockitoAnnotations.initMocks(this)
    }
}

除了顯式地調用MockitoAnnotations.initMocks(this)外,還可以使用MockitoJUnitRunner或者MockitoRule。使用方式如下:

@RunWith(MockitoJUnitRunner.StrictStubs::class)
class CalculatorTest {
    @Mock
    lateinit var calculator: Calculator
}

// or
class CalculatorTest {
    @Rule @JvmField
    val mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)

    @Mock
    lateinit var calculator: Calculator
}

還有 @InjectMocks 注解提供構造函數注入、setter 注入或成員注入,具體細節請看官網文檔

1.2 驗證方法調用

Mockito 中可以很方便地驗證mock 對象spy 對象的方法調用,通過verify方法即可:

val mockedList = Mockito.mock(mutableListOf<String>().javaClass)
mockedList.add("once")

mockedList.add("twice")
mockedList.add("twice")

mockedList.add("three times")
mockedList.add("three times")
mockedList.add("three times")

//following two verifications work exactly the same - times(1) is used by default
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once")

//exact number of invocations verification
verify(mockedList, times(2)).add("twice")
verify(mockedList, times(3)).add("three times")

//verification using never(). never() is an alias to times(0)
verify(mockedList, never()).add("never happened")

//verification using atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times")
verify(mockedList, atLeast(2)).add("three times")
verify(mockedList, atMost(5)).add("three times")

verify 方法使用非常簡便,但是還是有需要注意的地方,看下面測試代碼:

val mockedList = Mockito.mock(mutableListOf<String>().javaClass)
mockedList.add("twice")
verify(mockedList).add("twice")

mockedList.add("twice")
verify(mockedList).add("twice")

上面的測試代碼在第 5 行中的驗證會失敗,提示期望一次調用,實際上為兩次。第一次驗證時成功是沒問題的,第二次驗證時,此時mockedList.add("twice")執行了兩次,記錄為兩次,沒有隨著第一次驗證過后就刪除 1 次,所以會測試失敗。

驗證參數值

上面的驗證方法調用時,對于參數的校驗使用的默認 equals 方法,除此之外也可以使用 argument matchers:

verify(mockedList).add(anyString())
verify(mockedList).add(notNull())
verify(mockedList).add(argThat{ argument -> argument.length > 5 })

1.3 stubbing 指定方法的實現

除了驗證方法調用之外,Mockito 還有另外一個主要功能:指定方法的返回值或者實現。不過需要使用到 when 方法,而在 Kotlin 中 when 屬于關鍵字。

val mockedList = mock(mutableListOf<String>().javaClass)
// mockedList[0] 第一次返回 first,之后都會拋出異常
`when`(mockedList[0]).thenReturn("first")
        .thenThrow(IllegalArgumentException())
`when`(mockedList[1]).thenThrow(RuntimeException())
`when`(mockedList.set(anyInt(), anyString())).thenAnswer({ invocation ->
    val args = invocation.arguments
    println("set index ${args[0]} to ${args[1]}")
    args[1]
})

// use doThrow when stubbing void methods with exceptions
doThrow(RuntimeException()).`when`(mockedList).clear()
doReturn("third").`when`(mockedList)[2]

需要注意下 stubbing 方法的規則:

  • 一旦指定了方法的實現后,不管調用多少次,?該方法都是返回指定的返回值或者執行指定的方法

  • 當以相同的參數指定同一個方法多次時,最后一次指定才會生效

指定方法實現通常使用thenReturnthenThrowthenAnswer等,?因為這種方式更直觀。?但是?上面的例子中還有doReturndoThrowdoAnswer等 do 系列方法,它可以?實現 then 系列方法同樣的功能,不過在閱讀上沒有那么直觀。?在下面幾種情況下必須使用 do 系列方法:

  • 指定 void 方法

  • 指定 spy 對象的某些方法時

  • ?多次指定同一方法,以便在測試中途修改方法實現

其中第二條值得注意,當?使用 then 系列方法,spy 對象的實際方法其實還是會被調用的,然后才執行指定的實現,所以有時使用 then 系列方法會產生異常,這時只能使用 do 系列方法(它會覆蓋實際方法實現)。看下面這個例子:

val realList = mutableListOf<String>()
val spyList = spy(realList)

// stubbing success
`when`(spyList.size).thenReturn(5)

//Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
`when`(spyList[0]).thenReturn("first")

//You have to use doReturn() for stubbing
doReturn("first").`when`(spyList)[0]

2. mockito-kotlin 庫

Mockito 在 Kotlin 下使用時會有一些問題,例如 when 屬于關鍵字,??在參數驗證使用any()方法會返回 null,?在傳給非空類型參數時?會出錯。

??Github 上有人已經解決了這個問題,nhaarman 寫了一個??小而美的庫? Mockito-Kotlin ?幫助在 Kotlin 下方便地使用 Mockito。主要使用了頂層函數封裝了 Mockito 的常用靜態方法,如 mock()、any()、eq() 等。

?Gradle 引入

testImplementation 'com.nhaarman:mockito-kotlin:x.x.x'
// 使用 Kotlin 1.1 時
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:x.x.x'

下面是 mockito-kotlin 庫?所帶來的便利:

  • whenever 替換 when,避免與 ?Kotlin 關鍵字?沖突。
whenever(mock.stringValue()).thenReturn("test")
  • 創建 mock 或 spy 對象時,??如果類型可以推倒出來的話,不需要傳類型
val mock: MyClass = mock()

// if type cannot be inferred directly
val mock = mock<MyClass>()

// use mock object as parameter
val instance = MyClass(mock())
  • 可以在 mock 對象時指定方法實現
val mock = mock<MyClass> {
    on { stringValue() } doReturn "test"
}
  • ?對 Mockito 中 any()、eq() 這些?返回空的方法做了封裝,當調用any()時,會先調用Mockito.any()更新驗證狀態,然后返回一個非空的值,避免空指針問題。

?更多詳細的?內容,可以閱讀它的 wiki:mockito-kotlin Wiki

3. 小結

Mockito 框架和 mockito-kotlin 庫讓我們可以很方便地驗證 void 方法的輸出結果,即驗證方法的調用。?但是 Mockito 框架有一些限制,不能 mock 靜態方法,不能指定 final 方法的實現,不過這是利大于弊的,?讓我們不會濫用靜態方法,其實靜態方法應該只存在于 Utils 工具類中。本文只是大體介紹了 Mockito 的主要概念的,具體使用過程中遇到一些問題,推薦大家閱讀官方文檔。

到目前為止,介紹的 JUnit 4 和 Mockito 測試框架,都是針對 Java 或 Kotlin 代碼的測試,如果要在 JVM 中測試 Android 相關邏輯的話,需要利用到 Robolectric 測試框架,所以下一篇文章將介紹 Robolectric 的用法。

參考資料:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。