iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 5
2
Mobile Development

Android TDD 測試驅動開發系列 第 5

Day05 - 假物件 Stub、Mock

上一篇我們用了DI的技巧,建立了一個假物件來模擬天氣。
這一篇我們要來談談假物件。我們會將假物件分為Stub與Mock。

Stub:用來模擬外部相依物件的回傳結果。
Mock:用來驗證目標與相依物件的互動。

上一篇的模擬物件StubWeather,模擬外部相依物件回傳結果(晴天或雨天)的物件就是屬於Stub。

下圖說明了被測試物件與Stub的互動。
Test:測試程式。
SUT:System under test,被測試物件。
Stub:假物件,用來模擬外部相依物件的回傳結果。

https://ithelp.ithome.com.tw/upload/images/20190919/20111896QoAb8kagBQ.png

以上一篇雨傘計價的例子來對照上圖
Test:測試程式,UmbrellaTest.totalPrice_sunnyDay()
SUT:被測試物件,也就是Umebralla.totalPrice()用來計價的方法。
Stub:假天氣物件StubWeather,用來回傳預期是晴天或雨天。

https://ithelp.ithome.com.tw/upload/images/20190919/20111896FPQuPfnJaC.png

Mock

另一種模擬物件的方式叫Mock,Mock則是用來驗證目標與相依物件的互動

https://ithelp.ithome.com.tw/upload/images/20190919/20111896m0y1NVUGbi.png

我們用另一個範例來介紹什麼是Mock,這個範例延續賣雨傘的例子。我們用下訂單這個功能來作示範。
這邊的下訂單,先不討論新增到資料庫等。我們來討論一下寄送Email 給使用者這段。如果你在新增訂單時,會同時發送Mail給使用者。而這個發Mail的function又沒有回傳值讓你驗證是否有成功。那你要怎麼確保新增訂單有真的呼叫寄用Email給使用者的function,並傳入正確的參數。

這裡有一個成功訂單insertOrder的方法,裡面其中一段為寄送Mail給使用者

class Order {
    // 成立訂單
    fun insertOrder(email: String, quantity:Int, price: Int){
        val weather = Weather()
        val umbrella = Umbrella()
        umbrella.totalPrice(weather, quantity, price)

        //新增訂單...(省略)

        //寄送Email給使用者
        val emailUtil = EmailUtil()
        emailUtil.sendCustomer(email)
    }
}

我們需要測試成立訂單有沒有發mail。而這個寄送Email的function就是一個外部相依。我們需要驗證是否有與這個外部相依互動。

https://ithelp.ithome.com.tw/upload/images/20190919/20111896x6kvwoUrHi.png

開始寫測試

這個新增訂單的function一樣有相依物件的問題

  1. 依賴注入
  2. 建立Mock模擬物件,驗證SUT是否Mock互動

1. 依賴注入

將EmailUtil類別Extract Interface,新增IEmailUtil介面

interface IEmailUtil {
    fun sendCustomer(email: String)
}

class EmailUtil : IEmailUtil {
    override fun sendCustomer(email: String) {
        //假裝發Email
    }
}

把EmailUtil 提出到Constructor並改成介面IEmailUtil,我們又完成了依賴注入

fun insertOrder(email: String, quantity: Int, price: Int, emailUtil: IEmailUtil){
    val weather = Weather()
    val umbrella = Umbrella()
    umbrella.totalPrice(weather, quantity, price)

    //結帳...(省略)

    //寄送Email給客人
    emailUtil.sendCustomer(email)
}

2. 建立Mock模擬物件,驗證SUT是否Mock互動

可以來寫測試了,這個測試要測成立訂單有沒有發送mail,測試的重點在於有沒有成功呼叫到emailUtil.sendCustomer(email),這裡的emtailUtil傳入的型別是一個介面,也就是說哪個類別實作IEmailUtil.sendCustomer其實已經不重要了。這個方法相依於介面,而不相依於實體。

新增一個MockEmailUtil,我們要用這個假的EmailUtil來記錄是不是有被呼叫到

class MockEmailUtil :IEmailUtil{
    // receiveEmail 用來記錄由sendCustomer傳進來的Email
    var receiveEmail:String? = null
    override fun sendCustomer(email: String) {
        receiveEmail = email
    }
}

呼叫order.insertOrder,傳入mockEmailUtil,最後用mockEmailUtil.receiveEmail來驗證order.insert裡是否有呼叫IEmailUtil.setCustomer

@Test
fun testInsertOrder() {
    val order = Order()
    val mockEmailUtil = MockEmailUtil()

    val userEmail = "someMail@gmail.com"
    order.insertOrder(userEmail, 1, 200, mockEmailUtil)

    //用mockEmailUtil.receiveEmail來驗證order.insert裡是否有呼叫IEmailUtil.setCustomer
    Assert.assertEquals(userEmail, mockEmailUtil.receiveEmail)
}

從原本的EmailUtil.sendCustomer,改成相依於介面IEmailUtil.sendCustomer就可以讓發Email這段可被測試囉。

小結

驗證的方式,通常有這3種。

  • 驗證回傳值
  • 驗證物件狀態的改變
  • 驗證目標與相依物件的互動

前兩種驗證回傳值、驗證物件狀態的改變,遇到外部相依,通常會使用Stub來輔助驗證回傳值驗證物件狀態的改變。驗證目標與相依物件的互動,指的就是Mock。請儘量將互動測試作為你的最後選擇,你應該儘量使用驗證回傳值或驗證物件狀態,因為互動測試會讓測試變的複雜。如果一個測試只測一件事,就只能有一個Mock。如果一個測試存在多個Mock,代表你正在測試多件事情。

範例下載:
https://github.com/evanchen76/TDD_MockSample

參考
單元測試的藝術
30天快速上手TDD Unit Test - Stub, Mock, Fake 簡介

小技巧
Ctrl + R 執行上一個測試
Ctrl + Shift + R 執行目前的測試

下一篇會介紹隔離框架,這些框架會讓你在Mock或Stub變得很簡單。

出版書:

Android TDD 測試驅動開發:從 UnitTest、TDD 到 DevOps 實踐


上一篇
Day04 - 晴天9折,雨天沒折
下一篇
Day06 - Mock 框架:Mockito
系列文
Android TDD 測試驅動開發30

尚未有邦友留言

立即登入留言