分享一个对所有Activity做单元测试的思路

Desmond·灵魂代码家·现充帝·干工头·家艺同学说他的博客都快长草了,这篇文章差点被CJJ抢走,好在我们和嫁衣同学有PY交易。

这篇依然是嫁衣同学带来的单元测试系列,一名不会写TestCase的Android不是好的司机。

最近升级了一下我们的Support库,这影响比较大,应该好好测试。这种情况下单元测试能帮助什么呢?我觉得有一定操作空间,于是想做一个“启所有Activity看看会不会崩溃”的功能。

Idea 1 - 手动解析配合MonekyRunner

aapt有一个命令是解析一个apk的AndroidManifest,一开始我就从这上面下手:

aapt dump xmltree ${apkpath} AndroidManifest.xml

它会输出类似如下字样: (做了一定精简)

N: android=http://schemas.android.com/apk/res/android
  E: manifest (line=2)
    A: android:versionCode(0x0101021b)=(type 0x10)0x68fb0
    A: android:versionName(0x0101021c)="4.30.0-preview" (Raw: "4.30.0-preview")
    A: package="tv.danmaku.bili" (Raw: "tv.danmaku.bili")

    E: uses-permission (line=17)
      A: android:name(0x01010003)="android.permission.READ_EXTERNAL_STORAGE" (Raw: "android.permission.READ_EXTERNAL_STORAGE")

    E: application (line=41)
      A: android:theme(0x01010000)=@0x7f0d0007
      E: activity (line=51)
        A: android:theme(0x01010000)=@0x7f0d0047
        A: android:name(0x01010003)="com.desmond.test.MainActivity" (Raw: "tv.danmaku.bili.ui.splash.SplashActivity")

它会以一个类似AndroidManifest.xml树的形式打出信息,这样一来我们可以用python脚本来轻易地处理它的输出,利用正则匹配去匹配带有Activity名字的那一行,并解析出Activity名字列出来:


本来我是想配合Android的MonkeyRunner( https://developer.android.com/studio/test/monkeyrunner/index.html )去做的,启动每个Activity之后截屏保存,因为都是python写也会比较方便。但是想法太天真,它启动不了非export的Activity。就放弃了

其实一开始我很天真地很自然地想到了这个方法,虽然后面没用,但是也写在这里好了。

Idea 2 - Instrument测试

我尝试着使用Instrument Test来完成这个任务,在这个过程中找到了最终方案。

Android的test support库提供了一个ActivityTestRule,它的作用是保证每个test执行前启动指定Activity,执行后结束Activity。这下我们可以参考一下它的代码,它是怎么同步启动Activity的?

其实没什么神秘面纱,Instrumentation直接提供了同步启Activity的办法,我直接贴出关键代码好了:

实际上startActivitySync就是有一个对象锁,在startActivity后让它wait,然后在目标Activity启动时会调用Instrumentation.prePerformCreate,在这里向主线程添加一个IdleHandler( https://developer.android.com/reference/android/os/MessageQueue.IdleHandler.html ),在它里面notify这个锁达到同步启动的效果。

那我们就可以利用这段代码来干点事情,在InstrumentTest里面我们可以拿到context,于是就能产出如下一段代码:


我们在里面通过PackageManager的API来获取APK包名里的所有Activity,通过ActivityInfo里面的name来拿到这个Activity的class名,然后可以构造一个Intent,利用之前说的方法来同步启动它。

我们在ActivityThread里面看到,performLaunchActivity等生命周期回调都是被包围在try/catch里面的,如果目标Activity的onCreate/onStart/onResume里面崩溃了,会调用Instrumentation.onException函数,而Android的测试里面对应的Instrumentation是AndroidJUnitRunner,它继承了这个方法,并使测试失败,记录堆栈:

所以说,如果这个时候想启的Activity崩了,我们能够立即拿到反馈,从而得到测试的效果

但是事实往往没有这么简单,这时候有一个难题了:我们的Activity通常需要在Intent里面传入一些参数,如果不够造就是非法Intent,即使测试失败不能证明有问题。而这个时候的适配,往往就不是一个框架能够解决得了,需要一个团队里有良好的编码习惯(代码风格一致),或者足够的时间去写一些自定义注解做解析适配。

我们项目里的Activity基本都有一个static的createIntent方法,通过调用这个方法传入参数来构建Intent启动它。这时候我就又有一个小想法:既然能获取到这个Activity的class名,那我干脆反射大搞一通。

其实接下来的代码就没什么好放的了,关键就两个:

  1. 写一套变量生成的规则,依照自定义生成 -> 默认primitive/String构造的优先级来生成每个对象;
  2. 写一套生成Activity启动Intent的规则机制,很多时候不是依靠随机放几个变量就能构造出Intent,有些Activity需要跳过(比如微信的WXEntryActivity),有些Activity只要简单的start就好,有些Activity需要特殊变量构造,有些Activity就随便放变量就行。

以上两点通过一定时间的编码应该能比较容易写出,我这里大概放一下我的代码:


代码也不太多,各位读者可以自行实现,大概效果如下:

不过这个测试说实在话也无法保证很多东西,能测出一些比较低级的崩溃,还有能自动化测试所有Activity,要不是跑一下它我还不知道原来我们有一百多个Activity…就是适配麻烦了点,还有后续的改动要更新也比较麻烦,可以酌情应用。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值