簡述
在日常項(xiàng)目開發(fā)中渠啤,基本沒有什么機(jī)會用到Kotlin跛锌,幾個月前學(xué)習(xí)的語法,基本上都忘光了丧诺,于是自己強(qiáng)迫自己在寫Demo中使用Kotlin入桂,同時,在目前開發(fā)的項(xiàng)目中開了一個測試分支驳阎,用來補(bǔ)全之前沒有寫的測試代碼抗愁。
環(huán)境配置
1.MockAPI
單元測試中使用真實(shí)開發(fā)環(huán)境中的真實(shí)數(shù)據(jù)是不明智的,最好的方式是用本地的數(shù)據(jù)模擬網(wǎng)絡(luò)請求呵晚,比如說我們有這樣一個API蜘腌,聯(lián)網(wǎng)library我們選擇Retrofit:
//TestService
interface TestService {
@GET("/test/api")
abstract fun getUser(@Query("login") login: String): Observable<User>
}
我們本地mock這個API會返回這樣的Json數(shù)據(jù):
{
"login": "qingmei2",
"name": "qingmei"
}
對應(yīng)的data類:
class User(val name: String = "defaultName",
val login: String = "defaultLogin")
好的,接下來我們定義一個Asset類饵隙,負(fù)責(zé)管理本地Mock的API返回資源:
object MockAssest {
private val BASE_PATH = "app/src/test/java/cn/com/xxx/xxx/base/mocks/data"
//User API對應(yīng)的模擬json數(shù)據(jù)的文件路徑
val USER_DATA = BASE_PATH + "/userJson_test"
//通過文件路徑撮珠,讀取Json數(shù)據(jù)
fun readFile(path: String): String {
val content = file2String(File(path))
return content
}
//kotlin豐富的I/O API,我們可以通過file.readText(charset)直接獲取結(jié)果
fun file2String(f: File, charset: String = "UTF-8"): String {
return f.readText(Charsets.UTF_8)
}
}
關(guān)于Kotlin更多強(qiáng)大的IO操作的API,可以參考這篇:Kotlin IO操作
2.MockRetrofit
我們直接配置一個MockRetrofit進(jìn)行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一個對應(yīng)的API Service對象癞季。
3.對上面兩個tool類的測試
在測試自己的業(yè)務(wù)代碼之前劫瞳,我們當(dāng)然要先保證這兩個工具類的邏輯正確,如果這兩個腳手架都是錯誤的绷柒,那么接下來業(yè)務(wù)代碼的單元測試毫無意義志于。
- MockAsset.kt的Test
class MockAssetTest {
@Test
fun assetTest() {
//MockAssest讀取文件,該函數(shù)所得結(jié)果將來會作為模擬的網(wǎng)絡(luò)數(shù)據(jù)返回废睦,我們這個單元測試的意義
//就是保證模擬的網(wǎng)絡(luò)數(shù)據(jù)能夠正確的返回
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數(shù)據(jù)
val retrofit = MockRetrofit()
val service = retrofit.create(TestService::class.java)
retrofit.path = MockAssest.USER_DATA //設(shè)置Path,設(shè)置后嗜湃,retrofit會攔截API,并返回對應(yīng)Path下Json文件的數(shù)據(jù)
service.getUser("test")
.test()
.assertValue { it ->
it.login.equals("qingmei2")
it.name.equals("qingmei")
}
}
}
使用 Mockito-Kotlin
我嘗試使用這個新的庫奈应,mockito-kotlin:Using Mockito with Kotlin
我選擇這個基于Mockito之上的拓展庫的理由很簡單,更方便入門(我無法保證之后的測試代碼過程中會不會踩坑购披,但是首先我得能夠進(jìn)行單元測試)杖挣。
關(guān)于Mockito在Kotlin的使用中會遇到的一些問題,這篇文章也許會對你有些幫助:
在Kotlin上怎樣用Mockito2 mock final 類(KAD 23)
我沒有按照上面的步驟進(jìn)行配置的嘗試刚陡,但是當(dāng)我在使用mockito-kotlin踩到坑時惩妇,在這篇筆記中留下這樣一個后路株汉,也許不會讓我碰得頭破血流而束手無策。
Mock依賴
我的項(xiàng)目中使用MVVM的架構(gòu)歌殃,這意味著乔妈,ViewModel的測試至關(guān)重要。
首先我把一些常用的依賴放到了BaseViewModel中:
public class BaseViewModel {
@Inject
protected AccountManager accountManager;//賬戶相關(guān)
@Inject
protected ServiceManager serviceManager;//API相關(guān)
//保存不同的加載狀態(tài)
public final ObservableField<State> loadingState = new ObservableField<>(LOAD_WAIT);
...
...
...
}
我寫了一個BaseTestViewModel類氓皱,他繼承了BaseViewModel路召,這意味著同樣持有accountManager和serviceManager。
我在setUp函數(shù)中初始化了這兩個重要的對象波材,并進(jìn)行簡單的測試:
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之后股淡,我可以嘗試對我的不同業(yè)務(wù)代碼下的ViewModel進(jìn)行測試了。
交流
本文是簡單的嘗試下搭建的測試腳手架各聘,如果您有更好的方式或思路揣非,望請不吝指出。