测试技术之Android 单元测试实践
白羽 2018-05-21 来源 :网络 阅读 1036 评论 0

摘要: 本文将带你了解测试技术中的单元测试是什么?在计算机编程中,单元测试(Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。但是什么叫"程序单元"呢?是一个模块、还是一个类、还是一个方法(函数)呢?不同的人、不同的语言,都有不同的理解。一般的定义,尤其是是在OOP领域,是一个类的一个方法。在此,我们也这样理解:单元测试,是为了测试某一个类的某一个方法能否正常工作,而写的测试代码。希望本文对大家学测试技术有所帮助。


    什么是单元测试

  在计算机编程中,单元测试(Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。但是什么叫"程序单元"呢?是一个模块、还是一个类、还是一个方法(函数)呢?不同的人、不同的语言,都有不同的理解。一般的定义,尤其是是在OOP领域,是一个类的一个方法。在此,我们也这样理解:单元测试,是为了测试某一个类的某一个方法能否正常工作,而写的测试代码。


  单元测试的三个步骤:

  ●setup:即新建出待测试的类、设置一些前提条件等

  ●执行动作:即调用被测类的被测方法,并获取返回结果

  ●验证结果:验证获取的结果跟预期的结果是一样的


  单元测试不是集成测试

  这里需要强调一个观念,那就是单元测试只是测试一个方法单元,它不是测试一整个流程。举个例子来说,一个Login页面,上面有两个输入框和一个button。两个输入框分别用于输入用户名和密码。点击button以后,有一个UserManager会去执行performlogin操作,然后将结果返回,更新页面。那么我们给这个东西做单元测试的时候,不是测这一整个login流程。这种整个流程的测试:给两个输入框设置正确的用户名和密码,点击login button,最后页面得到更新。叫做集成测试,而不是单元测试。当然,集成测试也是有他的必要性的,然而这不是每个程序员应该花多少精力所在的地方。为什么是这样呢?因为集成测试设置起来很麻烦,运行起来很慢,在保证代码质量、改善代码设计方面更起不到任何作用,因此它的重要程度并不是那么高


  Android中的单元测试


  Android中的单元测试分为两种,Local Unit Tests 和 Instrumented Tests,前者运行在JVM,后者需要运行再Android设备


  Local Unit Tests

  Local Unit Tests运行在本地JVM,不需要安装APP,所以运行时间很快。也因此不能依赖Android的API,所以大多数时候需要用Mock的形式来做替换(后面会提到)



测试技术之Android 单元测试实践


  Local Unit Tests

  配置

  ●测试代码目录:module-name/src/test/java

  ●一般使用到的测试框架

  JUnit4

  Mockito

  使用Gradle添加相应的库


 

   dependencies {
      // Required -- JUnit 4 framework
      testCompile 'junit:junit:4.12'
      // Optional -- Mockito framework
      testCompile 'org.mockito:mockito-core:1.10.19'
  }

   


  使用


  JUnit4

  这是Java界用的最广泛,也是最基础的一个框架,其他的很多框架,包括我们后面会看到的Mockito,都是基于或兼容JUnit4的。 使用比较简单,最多的是其Assert类提供的assertXXX方法。

  假设这样的一个类需要测试


  public class Calculator {
      public int add(int one, int another) {
          return one + another;
      }
      public int multiply(int one, int another) {
          return one * another;
      }

   


  如果不使用单元测试框架,我们可能需要这样来验证这段代码的正确性:


  public class CalculatorTest {
      public static void main(String[] args) {
          Calculator calculator = new Calculator();
          int sum = calculator.add(1, 2);
          if(sum == 3) {
              System.out.println("add() works!")
          } else {
              System.out.println("add() does not works!")
          }
          int product = calculator.multiply(2, 4);
          if (product == 8) {
              System.out.println("multiply() works!")
          } else {
              System.out.println("multiply() does not works!")
          }
      }
  }

   


  然后我们再通过某种方式,比如命令行或IDE,运行这个CalculatorTest的main方法,在看着terminal的输出,才知道测试是通过还是失败。想一下,如果我们有很多的类,每个类都有很多方法,那么就要写一堆这样的代码,每个类对于一个含有main方法的test类,同时main方法里面会有一堆代码。这样既写起来痛苦,跑起来更痛苦,比如说,你怎么样一次性跑所有的测试类呢?所以,一个测试框架为我们做的最基本的事情,就是允许我们按照某种更简单的方式写测试代码,把每一个测试单元写在一个测试方法里面,然后它会自动找出所有的测试方法,并且根据你的需要,运行所有的测试方法,或者是运行单个测试方法,或者是运行部分测试方法等等。 对于上面的例子,如果使用Junit的话,我们可以按照如下的方式写测试代码:


  

public class CalculatorTest {
      Calculator mCalculator;
      @Before
      public void setup() {
          mCalculator = new Calculator();
      }
      @Test
      public void testAdd() throws Exception {
          int sum = calculator.add(1, 2);
          Assert.assertEquals(3, sum);
      }
      @Test
      public void testMultiply() throws Exception {
          int product = calculator.multiply(2, 4);
          Assert.assertEquals(8, product);
      }
  }


  上面的@Before修饰的方法,会在测试开始前调用,这里是新建了一个Calculator对象,所以之后的一些测试单元都可以直接使用这个实例,@Test修饰的方法,是用来需要测试的单元,例如testAdd方法是用来测试Calculator类的加法操作,测试单元内使用Assert类提供的assertXXX方法来验证结果。如果还有其他的测试方法,则以此类推。 另外还有一些可能需要用到的修饰符,如@After,@BeforeClass,@AfterClass等。


    Mockito

  Mockito的两个重要的功能是,验证Mock对象的方法的调用和可以指定mock对象的某些方法的行为。(对于不懂Mock概念的同学来说,第一次看到的确很可能很难理解)


  为什么要使用Mockito?

  这是项目中的一个例子:

  /**
   * @param  用于过滤的实体类型
   */
  public interface BaseItemFilter {
      /**
       * @param item
       * @return true:不过滤;false:需要过滤
       */
      boolean accept(T item);
  }

   


  BaseItemFilter是用来判断某种指定类型的实体是否需要过滤的,类似java中的FileFilter,目的是为了用了过滤不符合要求的实体。

  以下是我们的关键服务过滤器的实现:


  

public class EssentialProcessFilter implements BaseItemFilter {
      /**
       * 系统关键进程及用户主要的进程
       */
      private static HashSet sCoreList = new HashSet();
      /**
       * 加载系统核心进程列表
       * @param context
       */
      public static void loadCoreList(Context context) {
          if (sCoreList.isEmpty()) {

            

 final Resources r = context.getResources();
              String[] corePackages = r.getStringArray(R.array.default_core_list);
              Collections.addAll(sCoreList, corePackages);
          }
      }
      @Override
      public boolean accept(RunningAppBean appModle) {
          return appModle != null && !(isEssentialProcess(appModle.mPackageName) || isEssentialProcessMock(appModle.mPackageName, appModle.mIsSysApp));
      }
      /**
       * 判断进程是否属于重要进程
       * @param process

  

    * @return
       */
      public static boolean isEssentialProcess(String process) {
          return sCoreList.contains(process);
      }
      /**
       * 系统关键进程关键词模糊匹配
       * @param packageName
       * @param isSystemApp
       * @return
       */
      public static boolean isEssentialProcessMock(String packageName, boolean isSystemApp) {
          return 省略...额外的一些判断;
      }
  }

   


  可以看到,这里的关键服务的判断的判断规则可以分两部分,一个是从String.xml中预设的一段Arrays数组查找是否右符合的,这个需要在初始化或某个时机预先调用EssentialProcessFilter#loadCoreList(Context context)方法来加载,另外的一个判断是在EssentialProcessFilter#isEssentialProcessMock方法中定义,这个类中accept方法,定义了只要符合其中一种规则,那么我们就需要把它过滤。


  这个时候我们来写单元测试,你一开始就会发现你没有办法新建一个Context对象来读取String.xml,即使你想尽任何方法新建一个ContextImpl实例,最后你还是会出错的,主要原因再在于Gradle运行Local Unit Test 所使用的android.jar里面所有API都是空实现,并抛出异常的。 现在想想,我们实际上并不需要真的读取String.xml,我们需要验证的是记录在我们的关键列表集合是否生效,既然这样,我们前面说过了,Mockito的两个重要的功能是,验证Mock对象的方法的调用和可以指定mock对象的某些方法的行为。我们是否可以Mock一个Context对象并且指定它读取String.xml的行为?答案是可以的,如下就是使用Mockito的一段测试代码


  

public class TestListFilter2 {
      @Mock
      Context mContext;
      @Mock
      Resources mResources;
      @Before
      public void setup() {
          MockitoAnnotations.initMocks(this);
          Mockito.when(mContext.getResources()).thenReturn(mResources);
          Mockito.when(mResources.getStringArray(R.array.default_core_list)).thenReturn(getEssentialProcessArray());
          //模拟加载XML资源
          EssentialProcessFilter.loadCoreList(mContext);
      }
      /**
       * 测试关键服务的过滤器
       */
      @Test

    

 public void testEssentialFilter() {
          EssentialProcessFilter processFilter = new EssentialProcessFilter();
          ListFilter listFilter = Mockito.spy(ListFilter.class);
          listFilter.addFilter(processFilter);
          List list = new ArrayList();
          list.addAll(getEssentialAppBean());
          list.addAll(getNormalRunningApp());
          List result = Mockito.mock(ArrayList.class);
          for (RunningAppBean runningAppBean : list) {
              if (listFilter.accept(runningAppBean)) {
                  result.add(runningAppBean);
              }

        

 }
          Mockito.verify(listFilter, Mockito.times(list.size())).accept(Mockito.any(RunningAppBean.class));
          Mockito.verify(result, Mockito.times(getNormalRunningApp().size())).add(Mockito.any(RunningAppBean.class));
      }
      /**
      * 关键服务应用包名
      */
      public String[] getEssentialProcessArray() {
        return new String[]{"android.process.acore", "android.process.media", "android.tts", "android.uid.phone", "android.uid.shared", "android.uid.system"};
      }
    }


  上面的代码,我们使用@Mock来Mock了Context和Resource对象,这需要我们在setup的时候使用MockitoAnnotations.initMocks(this)方法来使得这些注解生效,如果再不使用@Mock注解的时候,我们还可以使用Mockito.mock方法来Mock对象。这里我们指定了Context对象在调用getResources方法的时候返回一个同样是Mock的Resources对象,这里的Resources对象,指定了在调用getStringArray(R.array.default_core_list)方法的时候返回的字符串数组的数据是通过我们的getEssentialProcessArray方法获得的,而不是真的是加载String.xml资源。最后调用EssentialProcessFilter.loadCoreList(mContext)方法使得EssentialProcessFilter内记录的关键服务集合的数据源就是我们指定的。目前,我们使用的就是改变Mock对象的行为的功能。


  在测试单元testEssentialFilter方法中,使用Mockito.spy(ListFilter.class)来Mock一个ListFilter对象(这是一个BaseItemFilter的实现,里面记录了BaseItemFilter的集合,用了记录一系列的过滤规则),这里使用spy方法Mock出来的对象除非指定它的行为,否者调用这个对象的默认实现,而使用mock方法Mock出来的对象,如果不指定对象的行为的话,所有非void方法都将返回默认值:int、long类型方法将返回0,boolean方法将返回false,对象方法将返回null等等,我们也同样可以使用@spy注解来Mock对象。这里的listFilter对象使用spy是为了使用默认的行为,确保accept方法的逻辑正确执行,而result对象使用mock方式来Mock,是因为我们不需要真的把过滤后的数据添加到集合中,而只需要验证这个Mock对象的add方法调用的多少次即可。

  最后就是对Mock对象的行为的验证,分别验证了listFilter#accept方法和result#add方法的执行次数,其中Mockito#any系列方法用来指定无论传入任何参数值。


  依赖注入,写出更容易测试的代码


  这里举一个新的例子,在使用MVP模式开发一个登录页

  这是我们的LoginPresenter,Presenter的职责是作为View和Model之间的桥梁,UserManager就是这里的Model层,它用来处理用户登录的业务逻辑


 

 public class LoginPresenter {
      private final LoginView mLoginView;
      private UserManager mUserManager = new UserManager();
      public LoginPresenter(LoginView loginView) {
            mLoginView = loginView;
      }
      public void login(String username, String password) {
          //....
          mLoginView.showLoginHint();
          mUserManager.performLogin(username, password);
          //...
      }

   


  这段代码存在了一些问题

  ●如果现在要改变UserManager生成方式,如需要用new UserManager(String config)初始化,需要修改LoginPresenter代码

  ●如果想测试不同UserManager对象对LoginPresenter的影响很困难,因为UserManager的初始化被写死在了LoginPresenter的构造函数中(现在的Mockito可以使用@InjectMocks来很大程度缓解这个问题)

  为了把依赖解耦,我们一般可以作如下改变


 

 public class LoginPresenter {
      private final LoginView mLoginView;
      private final UserManager mUserManager;
      //将UserManager作为构造方法参数传进来
      public LoginPresenter(LoginView loginView,UserManager userManager) {
          this.mLoginView = loginView;
          this.mUserManager = userManager;
      }
      public void login(String username, String password) {
          //... some other code
          mUserManager.performLogin(username, password);
      }
  }


  这就是一种常见的依赖注入方式,这种方式的好处是,依赖关系非常明显。你必须在创建这个类的时候,就提供必要的dependency。这从某种程度上来说,也是在说明这个类所完成的功能。实现依赖注入很简单,比较有名的Dagger、Dragger2这些框架可以让这种实现变得更加简单,简洁,优雅,有兴趣可以自行了解。 作出这种修改之后,我们就可以很容易的Mock出UserManager对象来对LoginPresenter做单元测试。

 


 本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标软件测试之测试技术频道!


本文由 @白羽 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程