iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 10
2
Mobile Development

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

Day10 - Mock Android Framework

  • 分享至 

  • xImage
  •  

我們曾提到當你的程式與Android framework相依,在測試時就會是Instrumented tests,需要在實體裝置或模擬器執行。這會讓你的測試變慢,這篇將介紹怎麼透過Mock Android framework,讓你仍是使用Local unit tests。

延續上一篇註冊的案例。這次我們想要讓使用者在註冊成功時,把註冊的帳號儲存在App裡。在需要的時候,就可以直接使用。
而儲存資料,第一個會想到要用的就是SharePreference了。

我們已經知道寫在Activity的不好測試,所以建立一個Repository來處理儲存帳號至SharedPreference的部分吧

class Repository(val context: Context) {
    //儲存UserId至SharePreference
    fun saveUserId(id: String) {
        val sharedPreference = context.getSharedPreferences("USER_DATA", Context.MODE_PRIVATE)
        sharedPreference.edit().putString("USER_ID", id).commit()
    }
}

在註冊成功時呼叫儲存。

//註冊成功,儲存Id
Repository(this).saveUserId(loginId)

測試repository.saveUserId()

在saveUserId()看到儲存SharedPreference會用到Context,就知道它應該會是一個Instrumented tests。但我們希望可以用Local unit tests就可以在JVM上測試,因為這樣測試的速度較快。

在 Gradle 加上Mockito 框架,我們將使用Mockito來模擬與SharedPreference的互動。還記得我們在介紹假物件Mock時,曾提到這樣的做法。

dependencies {
    testImplementation 'org.mockito:mockito-core:2.21.0'
    testImplementation 'org.mockito:mockito-inline:2.21.0’
}

測試步驟:

  1. Mock Context、SharePreference
  2. 使用when thenReturn 讓Production code 呼叫sharedPreference時回傳模擬的物件
  3. 執行被測試物件:Activity 呼叫repository.saveUserId()
  4. 使用verify method,驗證模擬物件是否有呼叫putString,並傳入正確的參數
  5. 檢查SharedPreference是否有呼叫commit
@Test
fun saveUserId() {
    //步驟1.Mock Context、SharePreference
    val sharedPrefs = mock(SharedPreferences::class.java)
    val sharedPrefsEditor = mock(SharedPreferences.Editor::class.java)
    val context = mock(Context::class.java)

    //步驟2. 使用when thenReturn 讓Production code 呼叫sharedPreference時回傳模擬的物件
    `when`(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs)
    `when`(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
    `when`(sharedPrefsEditor.putString(anyString(), anyString())).thenReturn(sharedPrefsEditor)

    val userId = "A1234567"
    val preKey = "USER_ID"

    //步驟3. 執行被測試物件:Act 呼叫repository.saveUserId()
    val repository = Repository(context)
    repository.saveUserId(userId)

    //步驟4. 使用verify method,驗證模擬物件是否有呼叫putString,並傳入正確的參數。
    verify(sharedPrefsEditor).putString(
        argThat { key -> key == preKey },
        argThat { value -> value == userId }
    )

    //步驟5. 檢查SharedPreference是否有呼叫commit
    verify(sharedPrefsEditor).commit()
}

點綠色三角型,執行測試。

可以看到測試通過,測試的時間是1秒6,這樣的Local tests比起直接用Instrument tests快多了。

https://ithelp.ithome.com.tw/upload/images/20190924/20111896Oyc9NOcHEy.png

練習

直接Mock SharePreference不一定是好的解法。這邊給大家一個練習,另一個做法是增加一個中介層SharePreferenceManager,改去測試有沒有傳送正確的資料到ISharePreferenceManager,而不是去每次都要Mock SharePreference。

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

下一篇我們就來看用搭配Instrument tests的話應該怎麼測試。


上一篇
Day09 - 第一個Android 單元測試
下一篇
Day11 - Instrumented Tests
系列文
Android TDD 測試驅動開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Robin Chien
iT邦新手 5 級 ‧ 2021-03-14 22:54:15

您好,
我想請問是不是要被測試的 function 一定要是 public 的才能夠被測試?
如果 saveUserId() 是一個 private function 那還有辦法測試它嗎?

看更多先前的回應...收起先前的回應...
evanchen iT邦新手 2 級 ‧ 2021-03-17 20:18:23 檢舉

應該是不能測試。不過其實private function是不需要測試的。因為private function應該至少被一個public function給呼叫到,而這個public function才是我們應該要測試的。

了解,但真的不會有狀況是只測很核心的 private function 嗎?(雖然這感覺比較像是 Unit test 而不是 Instrumented test 的範圍)
最近看到有些人會在 variable 或是 function 加上 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) ,這會不會是更有彈性的作法呢?

https://developer.android.com/reference/androidx/annotation/VisibleForTesting

evanchen iT邦新手 2 級 ‧ 2021-04-16 21:31:24 檢舉

如果是現有的private function要加上測試,當然可以直接加囉。一般之所以不會測private還跟用TDD的方式開發有關。TDD從需求去產生測試案例,先寫測試案例再完成產品程式碼。所以我不太會是去測你說的核心,而是從"需求"去寫測試。

你可以參考我的這個分享。
https://www.youtube.com/watch?v=1h7dIbByhcs

Even 大大,太感謝了,我的 TDD 能力有待加強/images/emoticon/emoticon06.gif

我要留言

立即登入留言