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

article/2025/10/9 8:27:54

前言:

负责公司的单元测试体系的搭建,大约有一两个月的时间了,从最初的框架的调研,到中期全员的培训,以及后期对几十个项目单元测试的引入和推进,也算是对安卓的单元测试有了一些初步的收获以及一些新的认知,因此写下这篇文章来进行一个记录和总结。

以下的所有内容纯属个人观点,欢迎讨论。

一.单元测试标准

1.测试维度

单元测试有很多维度,比如针对功能点的维度,或者针对方法的维度。那么我们的项目,该如何定义这个维度呢?

安卓源码的项目中,是以功能点的维度来写单元测试的,比如验证发送一个广播的功能,验证就是广播接收者是否收到通知。这其中的流程,包含广播发送到系统侧,系统侧接收和处理,系统侧通知应用,应用分发给接收者等四个步骤。对于安卓系统来说,发送广播后,其一定能保证接收到广播,但是这样单元测试就存在耦合度,因为如果系统侧代码有问题的话,整个流程是跑不通的。对于安卓的项目,很多点我们是不能使用原生的对象,而是需要使用Mock的对象,但是随着项目的耦合度增加,环节的增多,需要mock的对象和验证点会爆炸性的增长,导致后期的单元测试方法的成本会几何式增长。

当然,以功能点为维度的话,也会有其好的一方面,如果单元测试不通过,则代表着流程中必有一环出现了问题,更容易暴露出问题。

如果以方法为维度,则不会存在依赖和耦合的问题,但是覆盖的范围则会小很多。所以到底应该以功能点为维度,还是以方法为维度呢?

我认为,这个最终还是取决于项目的结构和形式。如果是UI级别的项目,项目复杂度相对较轻,或者使用了各种框架完成了视图绑定,这种项目,自然适合针对功能点的维度来写单元测试。但是如果项目耦合度比较高,复杂度较高的话,则应该选择基于方法的维度,对耦合的部分使用mock对象进行切割。

2.覆盖范围

功能点:核心功能点

首先,单元测试应该覆盖所有的核心功能点。验证一个方法的时候,并不是方法中所有的点都需要验证,就比如Activity中onCreate方法中的某些内容,比如针对onCreate写单元测试的时候,验证initView/initListener/init三个方法是否被执行其实并没有意义,我们应该写单元测试代码分别对这三个方法进行验证,这才是我们的业务逻辑点。

@Overrideprotected 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 {//这个方法被调用了多次@Overridepublic 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 {@Testpublic 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"));}

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

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取  


http://chatgpt.dhexx.cn/article/ooSe5CM5.shtml

相关文章

java 线程 js_js javascript 实现多线程

在讲之前&#xff0c;大家都知道js是基于单线程的&#xff0c;而这个线程就是浏览器的js引擎。 首先来看一下大家用的浏览器都具有那些线程吧。 假如我们要执行一些耗时的操作&#xff0c;比如加载一张很大的图片&#xff0c;我们可能需要一个进度条来让用户进行等待&#xff0…

js创建多线程

我们知道js是单线程的&#xff0c;如果把占用大量计算资源的代码&#xff0c;或者获取大量数据&#xff0c;耗时较多的请求分离出去&#xff0c;用一个单独的线程去处理&#xff0c;会不会提升页面的性能的&#xff0c;答案是肯定的&#xff0c;尤其是现在我们开发大多是单页应…

javascript-js实现多线程

在讲之前&#xff0c;大家都知道js是基于单线程的&#xff0c;而这个线程就是浏览器的js引擎。 首先来看一下大家用的浏览器都具有那些线程吧。 假如我们要执行一些耗时的操作&#xff0c;比如加载一张很大的图片&#xff0c;我们可能需要一个进度条来让用户进行等待&#xff…

html5多线程例子,javascript的单线程事件循环及多线程介绍

前言 其实我前面文章对于改变js的执行顺序及多线程都有相关介绍&#xff01;例如&#xff0c;我们可以用setTimeout(fn,0)改变代码执行循序&#xff0c;文章最后也提及了Event Loop(事件循环)。同时&#xff0c;js的Worker可以模拟实现多线程&#xff0c;我前面文章也有类似的应…

js 实现多线程

2019独角兽企业重金招聘Python工程师标准>>> 一、多线程理解 首先,我们要理解什么是多线程,百度百科上说:多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线…

javascript多线程

一、什么是多线程 二、Concurrent.Thread.js <meta charset"utf-8" /> <script src"Concurrent.Thread.js"></script> <script src"https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script><style&…

js 多线程Worker

任务执行优先级&#xff0c;微任务&#xff08;Promise&#xff09;>宏任务&#xff08;定时器&#xff09;>线程任务&#xff08;Worker&#xff09; 多线程处理多个任务&#xff0c;这里假设5个任务循环了80亿&#xff0c;如果按之前js单线程执行是不是5*80亿&#xf…

Javascript的单线程与多线程

目录 一、浏览器的线程和进程 1.浏览器的线程 2.浏览器是多进程的 二、Javascript是单线程的 1.异步Ajax也是单线程的 2.setInterval和setTimeout本质上并不是多线程 三、Web Worker支持多线程 1.多线程间数据交互 2.Web Worker的兼容性 3.Web Worker的使用限制 3.1同…

JavaScript多线程编程

浏览器端JavaScript是以单线程的方式执行的&#xff0c;也就是说JavaScript和UI渲染占用同一个主线程&#xff0c;那就意味着&#xff0c;如果JavaScript进行高负载的数据处理&#xff0c;UI渲染就很有可能被阻断&#xff0c;浏览器就会出现卡顿&#xff0c;降低了用户体验。 …

JavaScript多线程初步学习

一、多线程理解 首先&#xff0c;我们要理解什么是多线程&#xff0c;百度百科上说&#xff1a;多线程&#xff08;英语&#xff1a;multithreading&#xff09;&#xff0c;是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时…

JavaScript如何实现多线程?

今天看到一道面试题&#xff0c;问js如何实现多线程?下面来总结一下&#xff1a; 因为 JS是一种单线程语言&#xff0c;即使是一些异步的事件也是在JS的主线程上运行的。像setTimeout、ajax的异步请求&#xff0c;或者是dom元素的一些事件&#xff0c;都是在JS主线程执行的&a…

JS多线程

JS是多线程的吗&#xff1f; 多线程编程相信大家都很熟悉&#xff0c;比如在界面开发中&#xff0c;如果一个事件的响应需要较长时间&#xff0c;那么一般做法就是把事件处理程序写在另外一个线程中&#xff0c;在处理过程中&#xff0c;在界面上面显示类似进度条的元素。这样…

at24c16如何划分出多个读写区_mega32数组、内存以及AT24C16读写相关

主控&#xff1a;mega32 编译器&#xff1a;iar2.31E 这两天折腾一个模块程序&#xff0c;一个温度补偿参数&#xff0c;本来是72个字节&#xff0c;现在扩展了三倍&#xff0c;变成288个&#xff0c;然后各种问题出现了。 第一次修改时想当然&#xff0c;直接把两个用到的全局…

STC8H开发(十二): I2C驱动AT24C08,AT24C32系列EEPROM存储

目录 STC8H开发(一): 在Keil5中配置和使用FwLib_STC8封装库(图文详解)STC8H开发(二): 在Linux VSCode中配置和使用FwLib_STC8封装库(图文详解)STC8H开发(三): 基于FwLib_STC8的模数转换ADC介绍和演示用例说明STC8H开发(四): FwLib_STC8 封装库的介绍和使用注意事项STC8H开发(五…

STM32的硬件I2C与AT24C16

刚学STM32的时候就听闻STM32的硬件I2C存在重大bug&#xff0c;会导致运行卡死在等待ACK的过程中&#xff0c;所以一直以来对其避而远之&#xff0c;转而以模拟I2C取代之。最近这段时间一直在用STM32 CubeMX&#xff0c;图形化设置界面屡试不爽&#xff0c;连USB这种复杂外设都能…

STM32F030 硬件I2C驱动 AT24C16

网络上很多F1系列的ATC24的读写程序&#xff0c;但F0几乎没有。由于F0完全重写了I2C&#xff0c;所以以往的代码并不能直接使用&#xff0c;修改事件、接口上会浪费很多时间&#xff0c;特别是对于使用F0系列进行入门的新手。 在此十分感谢 畅学电子网 的对于AT24C16的资料&am…

EEPROM 之 AT24C16 - 备忘录

因为论坛里看到STM的I2C有点小bug&#xff0c;所以这里采用的是模拟I2C时序 【注】m0.6us表示的是这一段时间最小不能小于为0.6us&#xff0c;M0.6us表示的是这一段时间最大为0.6us 对AT24C16的操作有读和写&#xff0c;读又分为CURRENT ADDRESS READ、RANDOM READ、SEQUENTIAL…

S32K144:12.LPI2C驱动AT24C16

1.打开官方例程 2.修改引脚配置 3.时钟可按照实际情况修改&#xff0c;也可不用更改&#xff0c;本例时钟不做更改 4.配置LPI2C模块 设置从机地址&#xff1a;从机地址如下图所示&#xff0c;低三位表示为AT24C16的块地址&#xff0c;AT24C16将2KB的内存空间分为8个块&…

stm32cubemx I2C读取AT24C16

本文对如何使用stm32cube生成I2C工程不作说明&#xff0c;仅对在对AT24Cxx系列的使用时作出易忽略的说明&#xff1b; 1、at24cxx页面结构&#xff1a; 从该图可以看出16K&#xff08;bit&#xff09;共有128个页&#xff0c;每页由16byte构成。16k 128 * 16 * 8; 特别注意&…

STM32之 AT24C16(EEPROM)驱动代码(程序稳定,清晰明了)

AT24C16电路图 第一部分&#xff1a;IIC协议代码头文件(iic.h) #ifndef IIC_H #define IIC_H #include "stm32f10x.h" #include "sys.h" #include "delay.h"#define write 0 #define read 1//IIC总线地址接口定义 #define IIC_SCL PBout(7) #d…