Android 单元测试接入指南

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 = LinkedList()
  val spy: MutableList = 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 单元测试接入指南)