MockK:一款強大的 Kotlin Mocking Library (Part 2 / 4)

MockK 功能介紹:mockk, every, Annotation, verify

前情提要

在 Java 的世界裡面,說到單元測試,不得不提到 Mockito,它是目前 Java 最多人使用的 Mocking framework,但若來到 Kotlin 的世界呢?我們還能仰賴 Mockito 進行單元測試嗎?有沒有專門為 Kotlin 這款語言量身打造的 Mocking framework 呢?

這系列文章會手把手教你如何使用 MockK,讓你了解它好用的地方在哪裡?以及它解決了什麼樣的問題?一共分成 4 篇文章,包括以下內容:

  1. 用 Kotlin + Mockito 寫單元測試會碰到什麼問題?
  2. MockK 功能介紹:mockk, every, Annotation, verify(這篇)
  3. MockK 功能介紹:Relaxed Mocks, 再談 Verify , Capture
  4. 如何測試 Static Method, Singleton

看完前一篇文章提到的問題,心裡不免 OS:「只是想用 Kotlin 寫單元測試而已,有必要這麼折磨人嗎?」況且我連測試都還沒開始寫耶!如果可以不用處理這些問題又可以測試靜態方法,該有多好?

MockK

MockK 是一款專門為 Kotlin 所設計的 Mocking Library,Mockito 能做到的事情它都做得到,Mockito 做不到的事情它也做得到,簡而言之 MockK 可以讓你無痛地在 Kotlin 下使用 Mockito 跟 PowerMock 的功能,它是一款完全獨立的套件,你不會需要引入 Mockito 或 PowerMock,更不會被兼容性的問題綁架。

Gradle Setting

首先,在 build.gradle(app) 加入以下設定:

dependencies {
    testImplementation 'io.mockk:mockk:1.8.6'
    ...
}

Example: 小孩要零用錢

為方便講解,這邊會用小孩要零用錢這個案例來進行說明,每個小孩都會需要零用錢,通常小孩沒錢時就會跟媽媽要零用錢,於是我宣告了一個叫 Kid 的 Class:

Kid 的建構子會傳入 Mother 這個 Class,每一個小孩初始情況是沒有錢的,小孩有一個功能就是要零用錢 wantMoney(),每當他要零用錢時,媽媽就會呼叫 giveMoney() 這個方法給小孩零用錢。

而 Mother 這個 Class 很單純:

只要小孩跟媽媽要錢,媽媽就給他 100 元。

mockk, every

接著對 wantMoney() 進行測試,測試代碼如下:

首先,我們 Mock Mother 這個 Class:

val mother = mockk<Mother>()

在 MockK 想要 Mock 一個 Class 的時候,只要使用 mockk<Class>()即可,後面泛型填入想要 Mock 的 Class 。

接著 New 了一個 Kid 物件:

val kid = Kid(mother)

並用到了 every 這個方法:

every { mother.giveMoney() } returns 30

這邊的 every 你可以把它想成是 Mockito 的 when().thenReturn(),每當有人呼叫 mother.giveMoney() 這個方法,我就給他 30 元。也就是說,你改變了這個方法行為 ── 指定它要做什麼事情。

接著執行測試動作,並驗證小孩的錢是否改變:

// When
kid.wantMoney()
// Then
assertEquals(30, kid.money)

最後,小孩的錢確實被改成 30 元,測試通過!

Annotation

MockK 跟 Mockito 一樣,也支援 Annotation 的方式進行初始化:

只要在 @Before 方法裡面加上 MockKAnnotations.init(this) 就能在外面用 Annotation 的方式 Mock 物件。

需求變更:必須通知媽媽

萬一媽媽現在變得很嚴格,規定小孩每次要零用錢的時候,都要先通知媽媽才行,於是我們修改一下 Kid Class,在 wantMoney() 中加上 mother.inform(money)

Mother Class 則新增了 inform() 這個方法:

實作內容很簡單,就是印出 “媽媽我現在有 XX 元,我要跟你拿錢!” 而已。

Verify

針對上面的情況來說,我們要確保的是:小孩要零用錢的時候,同時也有通知媽媽,於是將測試代碼改成如下:

這邊只在倒數第二行加上:

verify { mother.inform(any()) }

當你想要驗證一段方法有沒有被呼叫到的時候可以使用 verify

MockKException: No answer found for: Mother(#1).inform(0)

接著運行一下測試代碼,你會得到這樣的訊息:

io.mockk.MockKException: no answer found for: Mother(#1).inform(0)

這是因為在 MockK 裡面,預設情況下 Mock 這個動作是很嚴謹的,你必須要指定所有的行為操作才行,在測試代碼前面加上這行:

every { mother.inform(any()) } just Runs

再次運行測試,通過!

上面這種做法隱藏著一個問題:「假設今天 Class 的方法有 100 個,那豈不是要指定到天荒地老了嗎?」有沒有辦法跟 Mockito 一樣不用指定行為也能做後續的 verify

下篇文章將會介紹如何解決這個問題: