之前章節有提到用Mockito來測試Android的時候常常會遇到JVM static靜態類別無法被測試的問題,可是偏偏不論Android SDK本身或是我們常用的Library、甚至於我們也常常因為static呼叫方式的方便所以使得Singleton的用法穿梭在我們的codebase當中。因為Mockito並不支援static的mock測試,所以Android的developer用mockito來做單元測試的話必須跳過singleton的部份或是利用PowerMockito來加強Mockito的使用,或是把被測試的部份改成不用static。不測試singleton會導致測試不完整,用PowerMockito來加強Mockito並不穩定會發生各種exception,由其是Kotlin來寫會有更多問題,如果把測試的code改成不用static又可能導致production code有不預期的意外發生。
大家可能會想為什麼static不被Mockito所支援?因為static在物件導向的設計裡並不是好的用法,怎麼說呢?你今天貪圖方便用上static或是singleton的用法,但它極有可能在process執行時期在程式任何地方被改動。因為static是全局變數,假設同一個singleton class你有在自定的worker thread使用它的話,就有可能在不預期的時間點被改動,可能在ActivityA去呼叫singleton,但是因為沒處理好,跑到了Activity B的時候它才被改動,而Activity B又在worker therad處理好前先去更動singleton裡的變數就有可能會出現問題。這也是Mockito不支援static的原因,因為單元測試用到static的話就不能保證測試結果,事實上它在production run的時候有可能在這個單元外被改動。
話雖如此,但我們實際作業使用上還是常常會用到它而且單元測試時又常常要使用它,不用上述處理方法還有什麼好方法?還好Mockk支援mock singleton的寫法,因為大家目前singleton的開發可能部份的code是用Kotlin的companion object寫法,一部份還在用Java static的寫法,所以這邊會同時對Java跟Kotlin的寫法舉例。假設我們有一個叫KotlinServer的class來提供UserNameList,同時也有一個JavaServer的class來提供UserNameList
class KotlinServer {
companion object {
val instance by lazy {
KotlinServer()
}
}
fun requestUserNameList(): List<String> {
return listOf("User A", "User B")
}
}
public class JavaServer {
private static JavaServer mInstance = null;
public synchronized static JavaServer getInstance() {
if (mInstance == null) {
mInstance = new JavaServer();
}
return mInstance;
}
public List<String> requestUserNameList() {
ArrayList<String> list = new ArrayList();
list.add("User A");
list.add("User B");
return list;
}
}
然後在production code裡有一個ServerManager來提供我們存取這兩個Server class
class ServerManager {
fun getUserListFromJava(): List<String> {
return JavaServer.getInstance().requestUserNameList()
}
fun getUserListFromKotlin(): List<String> {
return KotlinServer.instance.requestUserNameList()
}
}
如果我們在Activity去呼叫ServerManager去獲取這些userNameList()那我們要怎麼在測試時去mock singleton呢?我在這裡寫了一個測試class叫testStatic()
@Test
fun testStatic() {
val expected = listOf("Daniel Chen") //預期mock static後可被更改的回傳值
val serverManager = ServerManager() //被測試class
mockkStatic(JavaServer::class)
every { JavaServer.getInstance().requestUserNameList() }.returns(expected)
var actual = serverManager.getUserListFromJava() //測試項目
assertEquals(expected, actual) //比對
mockkObject(KotlinServer)
actual = serverManager.getUserListFromKotlin() //測試項目
every { KotlinServer.instance.requestUserNameList() }.returns(expected)
assertEquals(expected, actual) //比對
}
mockk提供了我們去mock JVM static的寫法是mockkStatic<java class>,所以我們要mock Java寫的static class只要用mockkStatic就可以了,這時mockk就會把JavaServer class的實體mock起來,等一下再呼叫它的時候就可以指定自定的回傳值。所以我們在測試中想把requestUserNameList改成回傳listOf("Daniel Chen")只要在every區塊指定一呼叫requestUserNameList就回傳expected value。
every { JavaServer.getInstance().requestUserNameList() }.returns(expected)
最後用assertEquals的方式來比對static的值是否真的有被更改,結果是test passed。
因為在Kotlin中我們使用singleton的寫法官方建議是用利用companion object類別,不是用JVM static的寫法。所以我們在KotlinServer class裡是用companion object去寫singleton,而mockk也提供了mock object的方法mockkObject<kotlin object>,一樣我們利用這個方式把KotlinServer mock起來,等一下再呼叫它的時候就可以指定自定的回傳值。一樣我們在測試中想把requestUserNameList改成回傳listOf("Daniel Chen")只要在every區塊指定一呼叫requestUserNameList就回傳expected value。
every { KotlinServer.instance.requestUserNameList() }.returns(expected)
最後用assertEquals的方式來比對companion object的值是否真的有被更改,結果是test passed。
這一章節介紹了mock靜態類別的用法,下一章節會繼續介紹常用的function callback和lambda要如何被單元測試。