Android 单元测试之 Mockk

1. 概述
1.1 背景


但是在 Kotlin 开发的Android程序中,使用这两个框架真的如在Java中这么好用吗?

事实上,许多Kotlin的Android程序员在使用 Mockito 时会遇到一些问题,例如:

  • ​​Mockito cannot mock/spy because : — final class​​ : 在Kotlin里任何Class预设都是Final,而Mockito预设是无法Mock final Class
  • ​​java.lang.IllegalStateException: anyObject() must not be null​​ :Mockito的 any() 、eq()等方法都是可能回传 null 的,而 Kotlin 是“空安全”的,显然它不能接受这些方法的
  • ​​when 要加上反引號才能使用​​ 这是因为 when 在 Kotlin 中是保留字
  • 必须使用​​PowerMockito​​ 才能测试静态方法,但是初始化繁琐,官方也不推荐,最最重要的是 Mockito 和 PowerMockito 是两个不同的团队开发的,这导致他们可能存在兼容性的问题

虽然这上面的问题,都是有解决方法的,但是显而易见的:任何在 Kotlin 上使用 Mockito 进行单元测试的程序员都会踩坑。

为什么不提供一个专门针对于 Kotlin 场景下的单元测试呢? 于是就有了 ​​mockk​​。

1.2 优势



解决上述出现的问题
支持 Kotlin,包括其语法糖


1.3 存在问题



目前官方文档给出了一些存在的问题:

PowerMock 如果需要和 Mockk 一起使用,则需要一个工作区 (​ ​https://github.com/mockk/mockk/issues/79#issuecomment-437646333​​)
Inline 方法不支持 mock。


2. 使用


2.1 导入


testImplementation "io.mockk:mockk:1.11.0"


2.2 一个例子


下面是官方文档的一个简单的示例,这是我们的被测试类:

class Car {
    fun drive(direction: Direction): Outcome {
        return Outcome.NO
    }
}

enum class Direction {
    NORTH,
    SOUTH,
    WEST,
    EAST
}

enum class Outcome {
    OK,
    NO
}



这是我们的测试类:

class CarTest : TestCase() {
    fun testDrive() {
        // mock car对象
        val car = mockk<Car>()

        // 设置监听
        every { car.drive(Direction.NORTH) } returns Outcome.OK

        // 执行
        car.drive(Direction.NORTH)

        // 验证
        verify { car.drive(Direction.NORTH) }

        // 双重验证
        confirmVerified(car)
    }
}


可以看到​​Mockk​​ 使用了 Lambda 语句,这让代码实现变的很美观。
其次​​every{..}​​​ 语句用来设置监听,在​​Mockito​​​ 中,它是​​when​​ ,其实作用是一样的,回调你想要的操作
使用​​verify{..}​​ 进行验证,这是各个测试框架都通用的字段
有个​​confirmVerified​​​ 用来确认你的mock对象有没有被执行,因为前面已经有​​verify​​ 语句了,这里相当于一个二次确认,加不加都没什么关系
mockk框架遵循 mock - 监听 - 执行 - 验证 的流程,所以如果你之前已经学习过 Mockito,那么你将更加容易上手mockk。

2.3 DSL

  • 2.3.1 mock 普通对象

通过语句 ​​mockk<T>(...)​​ 来mock一个对象,例如:

val car = mockk<Car>()



你可以使用 ​​mockk()​​ 来代替任何mock对象,比如说一个参数,下面我们要监听当执行调用某个方法时,返回一个 Car 实例:

every { ... } return  Car()


如果我们不想这样做(因为可能会因为实例化太麻烦),可以这样写:

every { ... } return  mockk()



我们可以在 ​​mockk<T>()​​ 构建时,填入一些参数,它的构造方法可填参数有这些:

inline fun <reified T : Any> mockk(
    name: String? = null,
    relaxed: Boolean = false,
    vararg moreInterfaces: KClass<*>,
    relaxUnitFun: Boolean = false,
    block: T.() -> Unit = {}
) {....}



简单列下它们的作用:

​​name​​ : mock对象的名称
​​relaxed​​: 是否对其代码进行依赖,默认为否,这个参数比较关键,后续会更细的讲解一下
​​moreInterfaces​​: 让这个mock出来的对象实现这些声明的接口
​​relaxUnitFun​​​:和​​relaxed​​ 差不多,但是只针对于 返回值是Unit 的方法, 后续会讲解一下
​​block​​​: 该语句块表示你在创建完 mock 对象后的操作,相当于​​.also{ ... }​​ 语句

  • 2.3.2 relaxed 和 relaxUnitFun


在 mock 一个对象时,这两个参数的意义是什么呢? 举个例子,我现在有一个被测类 Car,它依赖于一个 Engine:

class Car(private val engine: Engine) {

    fun getSpeed(): Int {
        return engine.getSpeed()
    }
}

class Engine {
    fun getSpeed(): Int {
        return calSpeed()
    }
    private fun calSpeed(): Int {
        return 30
    }
}



我们要测试 ​​getSpeed()​​,它依赖于 Engine 里的方法,所以我们需要 mockk 一下 Engine,那么写下下面的测试方法:

fun testCar() {
        // mock engine对象
        val engine = mockk<Engine>()
        val car = Car(engine)
        // 这里是私有方法设置监听的写法:
        every { engine["calSpeed"]() } returns 30
        val speed = car.getSpeed()
        assertEquals(speed, 30)
    }


但是这里我们报了一个错误: ​​io.mockk.MockKException: no answer found for: Engine(#1).getSpeed()​​

这是因为mockk是严格去执行每个方法,而 Engine虽然mock了出来,但是mockk并不知道 ​​Engine.getSpeed()​​ 需不需要往下执行,所以它抛出了一个错误。

网上说有三种,正式验证的是时候,只有两种是想要的结果,未确定原因。
方案一:将 Engine 的构建从 mock 改成 spy,因为spy可以真实模拟对象行为: ​​​engine = spyk<Engine>()​​​---验证通过

方案二:抛弃 calSpeed 方法, 使用 ​​every { engine.getSpeed() } returns 30​​---验证通过

方案三:在 mock Engine 时, 将 ​​relaxed​​​ 置为true, ​​engine = mockk<Engine>(relaxed = true)​​​ 这就是 ​​relaxed​​ 的作用,它真实模拟mock对象的所以方法,产生依赖---未验证通过。

而 ​​relaxedUnitFun​​​ 没有 ​​relaxed​​ 那么厉害去模拟所有方法,仅仅模拟空返回的方法。

  • 2.3.3 使用注解 mock

除了使用 ​​mockk<T>()​​​ mock,我们还可以使用 ​​@Mock​​ 注解来mock:

@MockK // 通过注解mock
  lateinit var car1: Car

  @RelaxedMockK // 通过注解mock,并设置relaxed
  lateinit var car2: Car

  @MockK(relaxUnitFun = true) // 通过注解mock,并设置relaxed
  lateinit var car3:



然后需要在启动的时候去初始化一下这些注解:

 @Before
 fun setup() {
     MockKAnnotations.init(this, relaxUnitFun = true)
 }

  • 2.3.4 every / coEvery

​​every{...}​​​ 语句 没有什么好解释的,它就是 Mockito 中的​​when​​,用来监听指定的代码语句,并做出接下来的动作,例如:

​​return value​​ 返回某个值
​​just Runs​​ 继续执行(仅用于 Unit 方法)
​​answer {}​​ 执行某个语句块
因为Kotlin中有 协程 这个特性(本质上是线程),所以单元测试在执行时可能会遇到执行协程中代码的问题,这个时候如果需要监听,则需要使用 ​​coEvery{ ... }​​​ 当然了除了 ​​coEvery{...}​​ , 还有 ​​coVerify{...}​​、 ​​coRun​​、 ​​coAssert​​ 、 ​​coAnswer​​、​​coInvoke​​ 等用于协程中的方法,后面就不再赘述了。

  • 2.3.5 verify

​​verify​​ 是用来检查方法是否触发,当然它也很强大,它有许多参数可选,来看看这些参数:

fun verify(
    ordering: Ordering = Ordering.UNORDERED,
    inverse: Boolean = false,
    atLeast: Int = 1,
    atMost: Int = Int.MAX_VALUE,
    exactly: Int = -1,
    timeout: Long = 0,
    verifyBlock: MockKVerificationScope.() -> Unit
){}



他们作用如下:

​​ordering​​​: 表示​​verify{ .. }​​ 中的内容(下面简称语句块)是按照顺序执行。 默认是无序的
​​inverse​​:如果为true,表示语句块中的内容不发生(即方法不执行)
​​atLeast​​:语句块中方法最少执行次数
​​atMost​​:语句块中方法最多执行次数
​​exactly​​:语句块中的方法具体执行次数
​​timeout​​:语句块内容执行时间,如果超过该事件,则测试会失败
​​verifyBlock​​: Lambda表达式,语句块本身
除了这些,还有别的 verify 语句,方便你使用:

​​verifySequence{...}​​:验证代码按顺序执行,而且要每一行的代码都要在语句块中指定出来。
​​verifyAll{...}​​:验证代码全部都执行,没有顺序的规定
​​verifyOrder{...}​​:验证代码按顺序执行

  • 2.3.6 capture 和 slot

可以使用 ​​slot​​ 来抓取某一个值, 我们来在 Car 和 Engine 加一些方法:

class Car(private val engine: Engine) {
    fun setSpeed(s: Int) {
        engine.setSpeed(s)
    }
}

class Engine {
    fun setSpeed(speed: Int) {
    }
}



接下来在 ​​Car.speed​​ 中抓取其传参:

fun testCar() {
        val engine = mockk<Engine>(relaxed = true)
        val car = Car(engine)
        // 使用 slot 来准备获取值
        val mySlot = slot<Int>()
        // 方法调用时, 抓取传参
        every { engine.setSpeed(capture(mySlot)) } just Runs

        car.setSpeed(8)
        // 使用 slot.captured 来获取值
        assertEquals(8, mySlot.captured)
    }

  • 2.3.7 mock 静态类

和 Mockito 差不多: ​​mockkStatic(StaticClass::class)​​

  • 2.3.8 mock Object类

Kotlin 中 Object 类使用较多,而使用 ​​Mockito​​ 时不好对其进行mock,而 mockk 自然是完全支持啦,它通过下面语句mock一个object类:

mockkObject(ObjectClass)


如果你要验证、执行 object类里面的私有方法,你需要在mock的时候指定一个值 ​​recordPrivateCalls​​, 它默认是false:

mockkObject(ObjectClass, recordPrivateCalls = true)



​​enum​​ 类也是一样的mock方式

  • 2.3.9 给mock对象设置私有属性

一般属性只分成共有和私有,对于共有来说比较简单,直接set就行了。

而私有属性的设置需要通过反射来实现,在 mockk 中,需要使用 ​​InternalPlatformDsl​​ 这个类:

InternalPlatformDsl.dynamicSetField(engine, "speed", 30)

  • 2.3.10 执行 mock对象私有方法


方法也是分成共有和私有,所以对于共有方法,直接调用就行了。

对于私有方法,也是通过反射来实现,也需要调用 ​​InternalPlatformDsl​​ 这个类:

InternalPlatformDsl.dynamicCall(engine, "calSpeed", arrayOf(), mockk())


传参说明如下:

mock的类
要调用的方法名
方法传参,用array集起来
协程实例,没有的话可以用​​mockk()​​ 来替代
注:普通的mock类是可以可以这样使用的,但是对于 Object 类,需要在mock的时候设置 ​​recordPrivateCalls​​ 为true:

mockkObject(ObjectClass, recordPrivateCalls = true)



这样才能访问、调用object的私有方法

  • 2.3.11 验证mock对象私有方法

验证是放在 ​​verify{...}​​ 中的,也是通过反射的方式来验证:

verify{ mockClass["privateFunName"](arg1, arg2, ...) }


主要分成三个部分:

mock类
中括号,里面填入双引号+私有方法名
小括号,里面填入传参,可以使用​​allAny<T>()​​​、​​mockk()​​ … 或你想要的传入的实参
注:mock的object类也需要设置 ​​recordPrivateCalls​​ 为true

3. 小结


mockk 是专门使用在kotlin上的单元测试套件,现在已经完全成熟,适用于绝大部分的开发场景中
解决mockito痛点, 在kotlin开发中,可以完全替代mockito和powermockito
更多用法请看下面的官方文档


4. 文章


​ ​官方文档​:MockK | mocking library for Kotlin

转载地址:Android 单元测试之 Mockk_51CTO博客_Android 单元测试

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值