• Android UT


    什么是单元测试?

    一个单元测试是一段自动化的代码,这段代码调用被测试的工作单元,然后对这个单元的单个最终结果的某些假设进行检验。

    单元测试思路:
    1.确认待测试的方法或对象
    2.为待测试的方法构造初始化条件
    3.调用(运行)该测试方法
    4.比较被测试方法的行为(结果)与预期的是否一致

    设置测试环境

    在 Android Studio 项目中,必须将本地单元测试的源文件存储在 module-name/src/test/java/ 中。当创建新项目时,此目录已存在。
    在应用的顶级 build.gradle 文件中,将以下库指定为依赖项:
    dependencies {
    testImplementation 'junit:junit:4.12'
    testImplementation "io.mockk:mockk:1.9.3"
    }

    Mock 的定义

    mock就是创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两个目的:

    • 1.验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
    • 2.指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作

    为什么使用Mock测试

    单元测试是为了验证代码运行正确性,注重的是代码的流程以及结果的正确与否。

    对比真实运行代码,可能其中有一些外部依赖的构建步骤相对麻烦,如果按照真实代码的构建规则构造出外部依赖,会大大增加单元测试的工作,代码也会参杂太多非测试部分的内容,测试用例显得复杂难懂。

    采用 Mock 框架,可以虚拟出一个外部依赖,只注重代码的流程与结果,真正地实现测试目的。

    MockK 介绍

    MockK 是一个用 Kotlin 写的 Mocking 框架。通过mockk<>(), mockkObject(), spyk()返回的对象就处于mock状态。只有处于这个状态的对象才能通过every{}对对象的行为进行Mock。mockk框架遵循 mock - 监听 - 执行 - 验证的流程。

    testImplementation "io.mockk:mockk:1.9.3"
    
    import io.mockk.*
    

    注解

    注解 描述
    @Test 测试注解,标记一个方法可以作为一个测试用例 。
    @Before Before注解表示,该方法必须在类中的每个测试之前执行,以便执行某些必要的先决条件
    @After After注释表示,该方法在每项测试后执行(如执行每一个测试后重置某些变量,删除临时变量等)。
    断言方法 作用
    assertTrue 真值断言
    assertFalse 假植断言
    assertEquals 相等断言
    assertNotEquals 不等断言
    assertNull 空值断言
    assertNotNull 非空值断言
    assertFail 断言失败

    Failure一般由单元测试使用断言方法判断失败所引起,表示测试点发现了问题,程序的输出结果与预期结果不符

    举个例子

    public class ServiceImplA implements Service {
        @Override
    public void doSomething1(String s) {
    }
    
    @Override
    public String doSomething2(String s) {
        return s;
    }
    private String name = null;
    private String setName(String name){
            this.name = name;
        }
    }
    

    mock 普通对象

    通过语句 mockk(...)来mock一个对象
    这个方法返回T的实例,该实例所有函数都为待mock状态,这些待mock状态的函数都不能直接调用,需要结合every{}语句mock对应方法后才能调用

    //返回ServiceImplA一个mock对象
    val mockk = mockk<ServiceImplA>()
    //mock指定方法
    every { mockk.doSomething1(any()) } returns Unit
    //调用被mock的方法
    mockk.doSomething1("")
    //该方法未通过every进行mock,会报错
    mockk.doSomething2("")
    

    every{...}语句 用来监听指定的代码语句,并做出接下来的动作,例如:

    • return value返回某个值
    • just Runs 继续执行(仅用于 Unit 方法)
    • answer {} 执行某个语句块

    当在测试代码中执行到every{...}中的方法时并不会真的去执行,而是直接返回returns之后的对象。也可以不加returns而使用every { } just Runs跳过执行。

    mockk Object类

    将指定对象转为可mock状态

    与mockk<>()的区别是返回的mock对象,允许mock行为跟真实行为并存,如果不主动mock,则执行真实行为。

    val serviceImplA = ServiceImplA()
        mockkObject(serviceImplA)
        every { serviceImplA.doSomething1(any()) } returns Unit
        //调用被mock方法
        serviceImplA.doSomething1("")
        //调用真实方法
        serviceImplA.doSomething2("")
    

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

    mockkObject(serviceImplA, recordPrivateCalls = true)
    

    给mock对象设置私有属性

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

    InternalPlatformDsl.dynamicSetField(serviceImplA, "name", "fyn")
    

    执行 mock对象私有方法

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

    InternalPlatformDsl.dynamicCall(serviceImplA, "setName", arrayOf("new name"))
    {mockk()}
    

    mock 静态类

    mockkStatic(serviceImplA::class)
    

    spyk() & spyk(T obj)

    返回T的spyk对象或者obj的spyk对象
    与mockk<>()的区别是,spyk<>()返回的对象是允许真实行为跟mock行为共存的,其表现跟mockkObject()相似

        //返回ServiceImplA的一个spyk对象
        val spyk = spyk<ServiceImplA>()
        every { spyk.doSomething1(any()) } returns Unit
        //调用mock方法
        spyk.doSomething1("123")
        //调用真实方法
        spyk.doSomething2("999")
    
    val serviceImplA = ServiceImplA()
    //返回serviceImplA对象被spyk后的对象,原对象不会改变
    val spyk1 = spyk(serviceImplA)
    //serviceImplA不是可mock状态,这里会报错
    //every { serviceImplA.doSomething1(any()) } returns Unit
    
    //mock
    every { spyk1.doSomething1(any()) } returns Unit
    //调用mock方法
    spyk1.doSomething1("999")
    //调用真实方法
    spyk1.doSomething2("999")
    

    returns

    作用是定制mock行为的结果

    val spyk = spyk<ServiceImplA>()
    //mock doSomething2,无论什么输入都返回111
    every { spyk.doSomething2(any()) } returns "111"
    
    val input = "222"
    //这里拿到的应该是111
    val mockkResult = spyk.doSomething2(input)
    println("mockk行为结果:$mockkResult")
    
    val real = ServiceImplA()
    //这里拿到的应该是222
    val realResult = real.doSomething2(input)
    println("mockk行为结果:$realResult")
    

    验证多个方法被调用

    如果想验证这两个方法执行了的话,可以把两个方法都放在 verify {…} 中进行验证。如果确认成功那么测试通过,否则报错。

    @Test
    fun test() {
         //返回ServiceImplA一个mock对象
         val mockk = mockk<ServiceImplA>()
        //mock指定方法,设置监听
        every { mockk.doSomething1(any()) } returns Unit
        every { mockk.doSomething2(any()) } returns Unit
        verify { 
            mockk.doSomething1("sfas")
            mockk.doSomething2("sfas")
        }
    }
    

    为无返回值的方法分配默认行为

    把 every {…} 后面的 Returns 换成 just Runs ,就可以让 MockK 为这个没有返回值的方法分配一个默认行为。

    @Test
    fun test() {
        val serviceImplA = mockk<ServiceImplA>()
        every { serviceImplA.doSomething1(any()) } just Runs
        verify { serviceImplA.doSomething1(any()) }
    }
    

    验证方法被调用的次数

    如果不仅想验证方法被调用,而且想验证该方法被调用的次数,可以在 verify 中指定 exatcly、atLeast 和 atMost 属性。

      // 验证调用了两次
        verify(exactly = 2) { serviceImplA.doSomething1(any()) }
      
        // 验证调用了最少一次
        // verify(atLeast = 1) { serviceImplA.doSomething1(any()) }
      
        // 验证最多调用了两次
        // verify(atMost = 2) { serviceImplA.doSomething1(any()) }
    

    验证 Mock 方法都被调用了

    verifyAll {
            serviceImplA.doSomething1(any())
            serviceImplA.doSomething2(any())
            serviceImplA.doSomething3(any())
            serviceImplA.doSomething4(any())
        }
    

    验证全部的 Mock 方法都按特定顺序被调用了

    如果不仅想测试好几个方法被调用了,而且想确保它们是按固定顺序被调用的,可以使用 verifySequence {…} 。

    verifySequence {
           serviceImplA.doSomething1(any())
            serviceImplA.doSomething2(any())
            serviceImplA.doSomething3(any())
            serviceImplA.doSomething4(any())
        }
    

    验证mock对象私有方法

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

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

    主要分成三个部分:

    1.mock类
    2.中括号,里面填入双引号+私有方法名
    3.小括号,里面填入传参
    注:mock的object类也需要设置 recordPrivateCalls 为true

    延迟验证

    使用 verify(timeout) {…} 就可以实现延迟验证,比如下面代码中的 timeout = 2000 就表示在 2 秒后检查该方法是否被调用。

     verify(timeout = 2000) { serviceImplA.doSomething1(any()) }
    

    assertEquals

    判断effectedNum和预期值1是否相同,如果不同则测试fail

    assertEquals(expected:1,effectedNum)
    

    relaxed

    有一个被测类 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()
    方案二:抛弃 calSpeed 方法, 使用 every { engine.getSpeed() } returns 30

    方案三:在 mock Engine 时, 将 relaxed 置为true, engine = mockk(relaxed = true)

  • 相关阅读:
    java解析xml的几种方式
    Android Studio 问题解决List
    Android 无线调试方法
    Android 单选/复选控件
    clr
    jquery.metadata.js使用分析
    更改新建Asp.net WebForm的模板 的方法
    获取定位数据
    简易水平仪实现
    简易指南针实现
  • 原文地址:https://www.cnblogs.com/fynnn/p/15656762.html
Copyright © 2020-2023  润新知