51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 654|回复: 0
打印 上一主题 下一主题

[资料] 分享基于安卓项目的单元测试总结

[复制链接]
  • TA的每日心情
    无聊
    1 小时前
  • 签到天数: 944 天

    连续签到: 3 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2023-6-27 13:16:29 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    前言:
      负责公司的单元测试体系的搭建,大约有一两个月的时间了,从最初的框架的调研,到中期全员的培训,以及后期对几十个项目单元测试的引入和推进,也算是对安卓的单元测试有了一些初步的收获以及一些新的认知,因此写下这篇文章来进行一个记录和总结。
      以下的所有内容纯属个人观点,欢迎讨论。
      一.单元测试标准
      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"));
      }


      通过单测代码,我们就可以清楚的知道,粘性广播是一种允许先发送,后注册也可以接收到的广播。

    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-5-8 10:16 , Processed in 0.066725 second(s), 22 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表