基于安卓项目的单元测试总结

发表于:2023-6-27 09:25

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:失落夏天    来源:CSDN

  前言:
  负责公司的单元测试体系的搭建,大约有一两个月的时间了,从最初的框架的调研,到中期全员的培训,以及后期对几十个项目单元测试的引入和推进,也算是对安卓的单元测试有了一些初步的收获以及一些新的认知,因此写下这篇文章来进行一个记录和总结。
  以下的所有内容纯属个人观点,欢迎讨论。
  一.单元测试标准
  1.测试维度
  单元测试有很多维度,比如针对功能点的维度,或者针对方法的维度。那么我们的项目,该如何定义这个维度呢?
  安卓源码的项目中,是以功能点的维度来写单元测试的,比如验证发送一个广播的功能,验证就是广播接收者是否收到通知。这其中的流程,包含广播发送到系统侧,系统侧接收和处理,系统侧通知应用,应用分发给接收者等四个步骤。对于安卓系统来说,发送广播后,其一定能保证接收到广播,但是这样单元测试就存在耦合度,因为如果系统侧代码有问题的话,整个流程是跑不通的。对于安卓的项目,很多点我们是不能使用原生的对象,而是需要使用Mock的对象,但是随着项目的耦合度增加,环节的增多,需要mock的对象和验证点会爆炸性的增长,导致后期的单元测试方法的成本会几何式增长。
  当然,以功能点为维度的话,也会有其好的一方面,如果单元测试不通过,则代表着流程中必有一环出现了问题,更容易暴露出问题。
  如果以方法为维度,则不会存在依赖和耦合的问题,但是覆盖的范围则会小很多。所以到底应该以功能点为维度,还是以方法为维度呢?
  我认为,这个最终还是取决于项目的结构和形式。如果是UI级别的项目,项目复杂度相对较轻,或者使用了各种框架完成了视图绑定,这种项目,自然适合针对功能点的维度来写单元测试。但是如果项目耦合度比较高,复杂度较高的话,则应该选择基于方法的维度,对耦合的部分使用mock对象进行切割。
  2.覆盖范围
  功能点:核心功能点
  首先,单元测试应该覆盖所有的核心功能点。验证一个方法的时候,并不是方法中所有的点都需要验证,就比如Activity中onCreate方法中的某些内容,比如针对onCreate写单元测试的时候,验证initView/initListener/init三个方法是否被执行其实并没有意义,我们应该写单元测试代码分别对这三个方法进行验证,这才是我们的业务逻辑点。
  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      initView();
      initListener();
      init();
  }
  另外,我们应该验证具体的实现逻辑方法,对于一些中转方法,也不应该验证,比如ActivityThread中的ApplicationThread类就不应该被验证。部分ApplicationThread中的代码参考:
  private class ApplicationThread extends IApplicationThread.Stub {
      public final void scheduleReceiver(Intent intent, ActivityInfo info,
          CompatibilityInfo compatInfo, int resultCode, String data, Bundle extras,
          boolean sync, int sendingUser, int processState) {
          updateProcessState(processState, false);
          ReceiverData r = new ReceiverData(intent, resultCode, data, extras,
                  sync, false, mAppThread.asBinder(), sendingUser);
          r.info = info;
          r.compatInfo = compatInfo;
          sendMessage(H.RECEIVER, r);
      }
      
      public final void scheduleCreateBackupAgent(ApplicationInfo app,
              CompatibilityInfo compatInfo, int backupMode, int userId, int operationType) {
          CreateBackupAgentData d = new CreateBackupAgentData();
          d.appInfo = app;
          d.compatInfo = compatInfo;
          d.backupMode = backupMode;
          d.userId = userId;
          d.operationType = operationType;
      
          sendMessage(H.CREATE_BACKUP_AGENT, d);
      }
      ...
  }
  3.覆盖率要求
  单元测试会有一个覆盖率,分别针对类,方法,行,大多数公司关注的是行代码覆盖率,并且会把其目标定在90%甚至95%的高目标。
  个人感觉,至少对于安卓的项目,这样的高覆盖率其实是不可取的。比如上面的例子中,安卓源码中的单元测试类ActivityThreadTest中,对ApplicationThread中的内容就完全没有验证,因为这部分代码属于对输入数据的组装和逻辑的分发,并没有什么可执行的逻辑。从我们单元测试作用的角度上讲,其并符合任何一条作用,因此对于这种代码写单元测试也是没有意义的。
  所以,单元测试应该覆盖的是我们的所有逻辑处理代码。如果是我来做的话,我会选择把ApplicationThread抽成单独的类,并且打上不需要单元测试的标记避免被统计在内。
  在这种场景下,我认为行覆盖率的目标应该设定在85%。未能覆盖的部分,包含不方便抽成单独类的部分,常量定义的部分等等。
  4.命名规范
  单元测试也是写代码,写代码就应该有一定的规范。
  参照安卓源码中的单元测试类,按照如下的方案来制定单元测试的命名规范更为合适。
  1).单元测试类对应被测试类,在被测试类后面添加Test代表是对其的单元测试。
  比如被测试类为ActivityA的话,则单元测试类的命名为ActivityATest。
  2).如果是以方法为维度的单元测试类,则对应的测试方法命名为:test+测试方法。
  比如被测试方法为methodA的话,则测试方法为testMethodA(驼峰命名)。
  一个测试方法可以覆盖多个源方法,同样,一个源方法也可以拆分成多个测试方法分别验证。
  3).如果是以功能点为维度的单元测试类,则对应的测试方法命名为:test+对应的功能点。
  比如验证广播能否发送到接收者,则其方法名为:testResult。
  二.单元测试的作用
  1.单元测试并不能有效提高项目质量
  了解到,有的公司用单元测试来替代集成测试甚至是黑盒测试,个人感觉是不可取的。也许,这些公司的单元测试的范围已经覆盖了部分的功能测试,但是单元测试终归是验证的方法级的功能点,如果强行的使其覆盖功能测试,会造成一些不好的效果。
  单元测试保证的是一个方法内的输入输出项,当项目开发新需求或者重构的时候,单元测试可以帮助我们快速识别到对原有项目的影响点,这才是单元测试应保证的内容。
  2.快速识别新功能的影响
  这个很容易理解,如果新的改动影响到了方法中原有的逻辑,则老的单元测试是跑不通的。这时候我们就需要判断,是按照需求修改单元测试用例,还是新写的逻辑有问题了。
  3.发现隐藏的问题
  这也属于单元测试推行过程中意外的收获。进入到某个页面后,refreshData方法被调用了两次,但是由于两次调用的逻辑都是一致的,所以单看表现,确实不知道这个方法被调用了两次。
  但是通过单元测试验证,验证方法执行此时是否为1,则验证出该方法被调用次数并不是1次,而从发现了这个多次调用的问题。
  //被检测的类型
  public class MVPPresenter implements IMVPActivityContract.IMainActivityPresenter {
      //这个方法被调用了多次
      @Override
      public void requestInfo() {
          //请求数据,订阅,并显示
          Consumer<InfoModel> consumer = this::processInfoAndRefreshPage;
          Flowable<InfoModel> observable = DataSource.getInstance().getDataInfo();
          Disposable disposable = observable
                  .subscribeOn(Schedulers.io())
                  .observeOn(AndroidSchedulers.mainThread())
                  .subscribe(consumer);
      }
  }
   
  //单元测试类
  public class MVPActivityTest {
      @Test
      public void testInit() {
          ...
          MVPPresenter mockPresenter = mock(MVPPresenter.class);
          ...
          //验证requestInfo方法被调用的此时是否是1次
          verify(mockPresenter, times(1)).requestInfo();
      }
  }
  4.督促我们解决项目中的耦合性
  单元测试可以很好的衡量项目耦合度。负责公司项目单元测试体系搭建的时候,和很多项目的负责人进行沟通,有很多人表示,单元测试的case十分难写。排查下来,无一例外,全部都是耦合度太高的原因。他们把大量的功能,以及需要分开执行的环节耦合到一个方法里面。
  比如BroadcastReceiver的onReceive方法中,不但执行参数的接收逻辑,还把后续的逻辑操作逻辑一并写在了onReceive方法中,甚至于有的还会在这里new一个线程去执行相关逻辑,这样的耦合度,单元测试方法必然是很难写的。反之,如果项目的耦合度很低,那么单元测试就会很好写。
  5.对方法的客观评价
  我们经常提到一个概念叫做圈复杂度,对于衡量方法内的圈复杂度,那么单元测试就是一个很好的指标。圈复杂度越高,单元测试代码中,其case就会越多。对于那种方法很短,但是验证case很多的,其圈复杂度往往就会很高。
  所以,通过对于单元测试代码的阅读,就可以很容易的衡量出其圈复杂度。
  6.注释的补充
  有的文章把单元测试称之为最好的注释,因为它可以对一个方法的输入和输出进行一个直观的展示,会比方法的注释更大的详细。但是在我看来,注释和单元测试应该各有各的优势,对于一个方法来说,单元测试的介绍固然比注释介绍的更清晰,但是同时也增加了我们的阅读成本。所以我更愿意称之为是注释的一个补充。
  如果只是为了reiview,或者对项目有一些初步的了解,那么注释就足够了。
  但是如果阅读源代码是为了在其基础上进行进一步的改造,那么就必须对原有方法有一个深入的了解,因为单元测试就是一个很好的工具。
  比如安卓源码中,对ContextImpl中对粘性广播sendStickyBroadcast的介绍有很多,但是反观其单元测试方法:
  public void testSetSticky() throws Exception {
      Intent intent = new Intent(LaunchpadActivity.BROADCAST_STICKY1, null);
      intent.putExtra("test", LaunchpadActivity.DATA_1);
      ActivityManager.getService().unbroadcastIntent(null, intent,
              UserHandle.myUserId());
   
      ActivityManager.broadcastStickyIntent(intent, UserHandle.myUserId());
      addIntermediate("finished-broadcast");
   
      IntentFilter filter = new IntentFilter(LaunchpadActivity.BROADCAST_STICKY1);
      Intent sticky = getContext().registerReceiver(null, filter);
      assertNotNull("Sticky not found", sticky);
      assertEquals(LaunchpadActivity.DATA_1, sticky.getStringExtra("test"));
  }
  通过单测代码,我们就可以清楚的知道,粘性广播是一种允许先发送,后注册也可以接收到的广播。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号