美文网首页
Android 单元测试接入指南

Android 单元测试接入指南

作者: 茶不思基 | 来源:发表于2020-08-23 23:59 被阅读0次

    Android 单元测试接入指南

    img

    基本介绍

    定义

    单元测试是验证 指定输⼊ 的 实际结果 是否与 预期结果 匹配的测试。

    接入单元测试的目的

    单元测试的好处

    • 提升代码的稳定性,保证代码的逻辑和边界均可覆盖

    • 自动化测试,利于自测与重构测试

    • 促进代码设计,让代码有明确的输入输出、各个层级间功能清晰

    单元测试的问题点

    • 部分情况会导致总开发时间更长

    • 功能更新,单元测试代码必须同步更新

    核心概念

    • 主体:通常为公共类的 public 方法,也即业务流程中的一个流程节点;

    • 目的:验证流程节点的处理结果是否与预期结果匹配;

    • 关注点:指定输入、预期结果、实际结果,不应关注测试对象任何内部流程任何内部细节

    • 用例个数:需覆盖输入与输出产生绝大部分的组合,并非一个测试主体一个用例的简单对应关系

    输入集、预期结果

    • 输入集:测试主体所有可能的输入集合类型,囊括正常输入、异常输入。输入集中多数输入在第一次编写流程节点的单元测试代码时确立,少数(多为异常输入)在后续迭代中伴随故障的产生而补充入输入集;

    • 预期结果:将预设的输入在 特定场景 下输入到流程节点后,期望得到的结果。预期结果可能是状态、单个行为、行为链,原则上预期结果不会是私有的状态、行为(private方法调用)。

    • 原则:输入与预期结果一一对应,有输入必有结果反馈;(这也就反射要求代码设计让有明确的分层和输入输出)

    测试分类

    按照预期结果的种类,可将单元测试分为两大类:

    • 状态测试:预期结果多为返回值、测试主体所在类暴露在外的成员变量。

    • 行为测试:预期结果多为特定行为(链),具体来说是其他 public 方法的调用:

      • 内部行为:为了防止内部嵌套测试的出现,对 public 做单元测试时方法内部其他的public方法,亦可作为预期结果;
      • 异常行为:异常输入产生异常结果,异常结果可能是抛出 Exception
      • 行为链:当指定输入可能产生多个需要关注的一系统行为时,测试的验证就应验证这一系列行为而非最后一次行为:

    单元测试的接入流程

    |

    img

    单元测试的维度

    按Android目录分类

    • androidTest 目录应包含在真实或虚拟设备上运行的测试。此类测试包括集成测试、端到端测试,以及仅靠 JVM 无法完成应用功能验证的其他测试。

    • test 目录应包含在本地计算机上运行的测试,如单元测试。

    按运行环境分类

    虚拟环境测试

    JUnit测试

    直接运行在PC端Java虚拟机环境中,只能测试标准的java包内容,不能测试Android的上下文,测试速度更快。

    模拟设备

    Robolectric等测试,在PC端模拟大部分的真机环境

    真机测试

    基本AndroidJUnit4的测试代码直接运行在手机设备上运行,几乎具有Android代码运行的所有上下文。用例在测试时,需要先安装原始的apk,同时AndroidJUnit4的代码会打包成另外一个apk,不过代码的运行是运行在原始apk的进程,因此能完全模拟真机的状态。同时还有一点需要注意,在运行一个测试用例时,原始的apk跑一遍apk启动的流程,类似的用户的所有初始化行为都会自动产生。

    img

    按测试内容分类

    img

    测试金字塔(如图 所示)说明了应用应如何包含三类测试(即小型、中型和大型测试):

    • 小型测试是指单元测试,用于验证应用的行为,一次验证一个类。

    • 中型测试是指集成测试,用于验证模块内堆栈级别之间的互动或相关模块之间的互动。

    • 大型测试是指端到端测试,用于验证跨越了应用的多个模块的用户操作流程。

    沿着金字塔逐级向上,从小型测试到大型测试,各类测试的保真度逐级提高,但维护和调试工作所需的执行时间和工作量也逐级增加。因此,您编写的单元测试应多于集成测试,集成测试应多于端到端测试。虽然各类测试的比例可能会因应用的用例不同而异,但我们通常建议各类测试所占比例如下:小型测试占 70%,中型测试占 20%,大型测试占 10%。

    单元测试基础实践

    基本框架介绍

    功能点 支撑框架 关键类、方法
    框架/容器 JUnit @Before、@After、@Test、@RunWith()
    状态验证 JUnit Assert#assertXxxx()
    行为验证 - 依赖Mock Mockito BDDMockito#given、BDDMockito#then
    行为验证 - 静态、私有Mock PowerMock PowerMockito
    四大组件测试 Roboletric
    完全真机模拟 AndroidJUnit4

    BDD编码规范

    Given(环境搭建)

    Given步骤需要搭建环境,为之后的操作提供测试基础。其操作可分为几大类:

    1. 测试主体所在类的创建;

    2. Mock 依赖类的注入;

    3. 将测试主体的状态设置为预期状态(如测试唤醒方法是否正确时,需先将语音置为休眠)

    4. 准备测试需要的数据;

    5. 插桩:当测试主体受依赖类某些方法的返回值、回调影响时,应对这些方法进行插桩操作。因为Mock之后的对象,并不会执行真实流程,通常无法给出有效的结果以致测试主体无法正常执行。

    When(执行测试)

    When步骤在Given步骤搭建的环境中,直接执行测试主体的调用,以触发需要的行为或者获得需要的状态。

    Then(结果验证)

    Then步骤拿到When步骤的执行结果后,需验证结果是否符合预期

    状态验证

    */***
     ** 说明:行为测试Demo*
     ** 典型场景:业务逻辑类的测试多数为⾏行行为测试,类中的依赖普遍呈现错综复杂的情况,通常都需要将其中的依赖都Mock 出对应的类以完成⾏行行为测试;*
     ** 验证原则:预期结果多为 验证返回值、测试主体所在类暴露在外的成员变量量*
     ** 推荐流程:Given(环境搭建)  ->  When(执⾏行行测试)  ->  Then(结果验证)*
     *** ***@author\*** *wangshengxing*  *08.20* *2020*
     **/*
    class StandardStateTest {
      val  TAG = **"StandardStateTest"**
      init {
        TesterLog.init()
      }
      */***
       ** 测试对象*
       ** 示例验证除法的正确性*
       **/*
      fun divide(a:Int,b:Int):Int{
        return a/b
      }
      */***
       ** Demo:测试除方法是否正常*
       **/*
      **@Test**
      fun canDivideWork(){
        //given
        val a=10
        val b=5
        val expect=2
        //when
        val result=divide(a,b)
        L.i(TAG, **"canDivideWork:** ${result}**"**)
        //then
        Assert.assertEquals(expect,result)
      }
    }
    

    真机状态验证

    */***
     ** 说明:加解密工具验证*
     *** ***@author\*** *wangshengxing*  *08.19* *2020*
     **/*
    **@RunWith**(AndroidJUnit4::class)
    class CipherUnitTest {
      **@Test**
      fun encryptDecryptWork() {
        //given
        val str=**"123456"**
        //when
        val encryptData= CipherUtil.encryptData(str)
        val decryptData= CipherUtil.decryptData(encryptData)
        //then
        Assert.assertEquals(str, decryptData)
      }
    }
    

    单元测试进阶

    插桩之spy与mock

    插桩指对原有的代码行为进行定制修改,通常有spy和mock两种形式。mock方法和spy方法都可以对对象进行插桩。但是前者是接管了对象的全部方法,而后者只是将有桩实现(stubbing)的调用进行mock,其余方法仍然是实际调用。

    spy的标准是:如果不打桩,默认执行真实的方法,如果打桩则返回桩实现。

    */***
     ** 对部分代码进行插桩*
     **/*
    **@Test**
    fun canSpyList(){
      //given
      val list: MutableList<String> = LinkedList<String>()
      val spy: MutableList<String> = spy(list)
      `when`(spy.size).thenReturn(100)
      //when
      spy.add(**"one"**)
      spy.add(**"two"**)
      L.i(TAG, **"canSpyList: list size** ${spy.size}**"**)
      //then
      Assert.assertEquals(spy[0], **"one"**)
      Assert.assertEquals(100, spy.size)
    }
    

    行为验证

    */***
     ** 说明:状态测试Demo*
     ** 典型场景:⼯工具类的测试偏向于状态测试。⼯工具类不不处理理具体业务,只提供算法、业务⽆无关的操作等,验证其返回结果即可完成测试;*
     ** 验证原则:*
     **    行为是否执行;*
     **    行为执行次数是否符合预期;*
     **    行为执行时的参数是否符合预期;*
     **    行为如果指定了监听器,监听器中操作是否符合预期;*
     *** ***@author\*** *wangshengxing*  *08.20* *2020*
     **/*
    class StandardBehaviorTest {
      val  TAG = **"StandardBehaviorTest"**
      object NetUtil{
        fun hasConnected()=true
      }
      interface LoginListener{
        fun onLogin(name:String,password:String)
        fun onFailed()
      }
      class DemoModel{
        fun login(name:String,password:String,listener:LoginListener){
          if (NetUtil.hasConnected()) {
            //...
            listener.onLogin(name, password)
          }else{
            listener.onFailed()
          }
        }
      }
      init {
        TesterLog.init()
      }
      **@Test**
      fun canLoginWhenAllNormal(){
        //given
        val name=**"user"**
        val password=**"123456"**
        val listener = mock(LoginListener::class.*java*)
        val model=DemoModel()
        //when
        model.login(name, password, listener)
        //then 验证函数成功调用
        then(listener).should().onLogin(Mockito.anyString(),Mockito.anyString())
        //验证调用次数
        then(listener).should(Mockito.times(1)).onLogin(name,password)
        //Mockito.any<>() 返回值为null ,因此需要自己定义
        then(listener).should(Mockito.timeout(2000)).onLogin(UT.any(),UT.any())
        //验证onFailed没有调用过
        then(listener).should(Mockito.times(0)).onFailed()
        Mockito.verify(listener, Mockito.never()).onFailed()
      }
    }
    

    相关文章

      网友评论

          本文标题:Android 单元测试接入指南

          本文链接:https://www.haomeiwen.com/subject/rchgjktx.html