1. 程式人生 > >Android單元測試:使用本地資料測試Retrofit

Android單元測試:使用本地資料測試Retrofit

簡述

在日常專案開發中,基本沒有什麼機會用到Kotlin,幾個月前學習的語法,基本上都忘光了,於是自己強迫自己在寫Demo中使用Kotlin,同時,在目前開發的專案中開了一個測試分支,用來補全之前沒有寫的測試程式碼。

筆者的Android單元測試相關係列:

環境配置

1.MockAPI

單元測試中使用真實開發環境中的真實資料是不明智的,最好的方式是用本地的資料模擬網路請求,比如說我們有這樣一個API,聯網library我們選擇Retrofit:

//TestService
interface TestService {

    @GET("/test/api"
) abstract fun getUser(@Query("login") login: String): Observable<User> }

我們本地mock這個API會返回這樣的Json資料:

{
    "login": "qingmei2",
    "name": "qingmei"
}

對應的data類:

class User(val name: String = "defaultName",
           val login: String = "defaultLogin")

好的,接下來我們定義一個Asset類,負責管理本地Mock的API返回資源:

object MockAssest {

    private val BASE_PATH = "app/src/test/java/cn/com/xxx/xxx/base/mocks/data"

    //User API對應的模擬json資料的檔案路徑
    val USER_DATA = BASE_PATH + "/userJson_test"

    //通過檔案路徑,讀取Json資料
    fun readFile(path: String): String {
        val content = file2String(File(path))
        return content
    }
    //kotlin豐富的I/O API,我們可以通過file.readText(charset)直接獲取結果
fun file2String(f: File, charset: String = "UTF-8"): String { return f.readText(Charsets.UTF_8) } }

關於Kotlin更多強大的IO操作的API,可以參考這篇:Kotlin IO操作

2.MockRetrofit

我們直接配置一個MockRetrofit進行API的攔截:

class MockRetrofit {

    var path: String = ""

    fun <T> create(clazz: Class<T>): T {

        val client = OkHttpClient.Builder()
                .addInterceptor(Interceptor { chain ->
                    val content = MockAssest.readFile(path)
                    val body = ResponseBody.create(MediaType.parse("application/x-www-form-urlencoded"), content)
                    val response = Response.Builder()
                            .request(chain.request())
                            .protocol(Protocol.HTTP_1_1)
                            .code(200)
                            .body(body)
                            .message("Test Message")
                            .build()
                    response
                }).build()

        val retrofit = Retrofit.Builder()
                .baseUrl("http://api.***.com")
                .client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build()
        return retrofit.create(clazz)
    }
}

這樣我們直接通過MockRetrofit.create(APIService.class)直接mock一個對應的API Service物件。

3.對上面兩個tool類的測試

在測試自己的業務程式碼之前,我們當然要先保證這兩個工具類的邏輯正確,如果這兩個腳手架都是錯誤的,那麼接下來業務程式碼的單元測試毫無意義。

  • MockAsset.kt的Test
class MockAssetTest {

    @Test
    fun assetTest() {
        //MockAssest讀取檔案,該函式所得結果將來會作為模擬的網路資料返回,我們這個單元測試的意義
        //就是保證模擬的網路資料能夠正確的返回
        val content = MockAssest.readFile(MockAssest.USER_DATA)
        Observable.just(content)
                .test()
                .assertValue("{\n" + "    \"login\": \"qingmei2\",\n" + "    \"name\": \"qingmei\"\n" + "}")
    }
}
  • MockRetrofit.kt的Test
class MockRetrofitTest {

    @Test
    fun mockRetrofitTest() {
        // 這個測試是保證Retrofit能夠成功攔截API請求,並返回本地的Mock資料
        val retrofit = MockRetrofit()
        val service = retrofit.create(TestService::class.java)
        retrofit.path = MockAssest.USER_DATA  //設定Path,設定後,retrofit會攔截API,並返回對應Path下Json檔案的資料

        service.getUser("test")
                .test()
                .assertValue { it ->
                    it.login.equals("qingmei2")
                    it.name.equals("qingmei")
                }
    }
}

使用 Mockito-Kotlin

我選擇這個基於Mockito之上的拓展庫的理由很簡單,更方便入門(我無法保證之後的測試程式碼過程中會不會踩坑,但是首先我得能夠進行單元測試)。

關於Mockito在Kotlin的使用中會遇到的一些問題,這篇文章也許會對你有些幫助:

我沒有按照上面的步驟進行配置的嘗試,但是當我在使用mockito-kotlin踩到坑時,在這篇筆記中留下這樣一個後路,也許不會讓我碰得頭破血流而束手無策。

Mock依賴

我的專案中使用MVVM的架構,這意味著,ViewModel的測試至關重要。

首先我把一些常用的依賴放到了BaseViewModel中:

public class BaseViewModel {

    @Inject
    protected AccountManager accountManager;//賬戶相關
    @Inject
    protected ServiceManager serviceManager;//API相關

    //儲存不同的載入狀態
    public final ObservableField<State> loadingState = new ObservableField<>(LOAD_WAIT);
    ...
    ...
    ...
}

我寫了一個BaseTestViewModel類,他繼承了BaseViewModel,這意味著同樣持有accountManager和serviceManager。

我在setUp函式中初始化了這兩個重要的物件,並進行簡單的測試:

open class BaseTestViewModel : BaseViewModel() {

    @Before
    fun setUp() {
        accountManager = mock()
        serviceManager = mock()
    }

    //測試accountManager 成功Mock
    @Test
    fun testAccountManager() {
        Assert.assertNotNull(accountManager)
        whenever(accountManager.toString()).thenReturn("mock AccountManager.ToString!")
        Assert.assertEquals(accountManager.toString(), "mock AccountManager.ToString!")
    }

    //測試serviceManager 成功Mock
    @Test
    fun testServiceManager() {
        Assert.assertNotNull(serviceManager)
        val alertService = mock<AlertService>()
        whenever(alertService.toString()).thenReturn("mock alertService")
        whenever(serviceManager.alertService).thenReturn(alertService)
        Assert.assertEquals(serviceManager.alertService.toString(), "mock alertService")
    }

    class TestViewModel : BaseTestViewModel() {
        //測試BaseTestViewModel的子類也能成功持有mock好了的accountManager
        @Test
        fun testSubTestClass() {
            Assert.assertNotNull(accountManager)
            whenever(accountManager.toString()).thenReturn("mock AccountManager.Sub ToString!")
            Assert.assertEquals(accountManager.toString(), "mock AccountManager.Sub ToString!")
        }
    }
}

這幾個測試pass之後,我可以嘗試對我的不同業務程式碼下的ViewModel進行測試了。

交流

本文是簡單的嘗試下搭建的測試腳手架,使用本地資料單元測試Retrofit的介面,如果您有更好的方式或思路,望請不吝指出。