Android Kotlin使用Mockito进行单元测试

简述

在日常项目开发中,基本没有什么机会用到Kotlin,几个月前学习的语法,基本上都忘光了,于是自己强迫自己在写Demo中使用Kotlin,同时,在目前开发的项目中开了一个测试分支,用来补全之前没有写的测试代码。

环境配置

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-kotlin:Using Mockito with Kotlin

我选择这个基于Mockito之上的拓展库的理由很简单,更方便入门(我无法保证之后的测试代码过程中会不会踩坑,但是首先我得能够进行单元测试)。

关于Mockito在Kotlin的使用中会遇到的一些问题,这篇文章也许会对你有些帮助:

在Kotlin上怎样用Mockito2 mock final 类(KAD 23)

我没有按照上面的步骤进行配置的尝试,但是当我在使用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进行测试了。

交流

本文是简单的尝试下搭建的测试脚手架,如果您有更好的方式或思路,望请不吝指出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容