基于 Robotium 的自动遍历方案

前排提醒 :文章稍微长了点,文字占比也高,但是读完你会感觉花费的时间是值得的!

前言

​ 做这个的初衷是发现项目中的崩溃问题(即稳定性)。Monkey达不到全覆盖,也试过思寒的AppCrawler,无奈速度上不太理想。我需要的是更快的反馈结果,于是乎着手自己写一个方案,也当做是提高编码能力,或者说对Android有更深入的理解。

解决了什么

​ 初期目标是想替代Monkey,众所周知Monkey的随机点击,以及不可控性,并不能做到完整的遍历。所以当下最主要的功能是发现崩溃问题 (如兼容性、混淆、代码问题导致的崩溃),额外可以做的是发现无数据时的空白布局(配合接口工具,启用快速模式验证)、发现无网络时是否显示无网络的布局(关闭网络,启用快速模式)等等。

使用效果

​ 在我们的产品上,启用爬虫模式试跑了几个小时发现了5个崩溃问题。当然发现第一个崩溃时自动遍历就停止了,它依赖于被测应用,被测应用崩溃,它也会一同退出,这是接下来要解决的问题(增加重启机制 )。当崩溃问题不予修复时,继续遍历,还是会走到第一个崩溃(可复现性 ),此时可以把崩溃的Activity加入忽略列表。

崩溃问题:

  • 发现的崩溃问题都是正常操作的,非异常操作(举例:非人类手速的点击等等)。换句话说就是用户也会遇到该问题

  • 擅长发现异步请求导致的崩溃问题

    • 异步请求拿到数据后更新UI,由于UI被销毁导致的崩溃

    • 为什么说是擅长?

      • 遍历逻辑基于Activity,点击View跳出本Activity后按返回键回到遍历Activity
      • 当网络不稳定时,数据返回延时加长,View销毁了数据才回来,如果此时代码没处理好就会发生崩溃
      • 建议遍历时切换到弱网环境

特性

  • 可跨应用

    • 补上Robotium不支持跨应用的短板
    • 自动遍历时不会有具体的跨应用操作,唯一出现的地方在Android 6.0以上版本启动应用时的授权操作(可能存在兼容性问题)
    • 跨应用应用在单独写用例时
    • 跨应用详情
  • 多种模式

    • 快速模式:只启动Activity,快速检测崩溃问题(如兼容性、混淆、代码问题导致的崩溃),一般几分钟可完成。依赖于Params.json文件,该文件可由录制模式产生。
    • 迭代模式:启动Activity并点击每个View。依赖于Params.json文件,该文件可由录制模式产生。
    • 爬虫模式:通过迭代主页并记录新开Activity,迭代完毕后读取新开Activity,循环往复,直至无新的Activity。
    • 录制模式:需人工操作应用,记录每个新开的Activity,供快速模式、迭代模式使用。录制模式可在功能测试阶段使用,录制模式默认休眠1个小时,期间操作应用打开的Activity都将被记录下来。
  • 智能输入

    • 根据输入框支持的输入类型和最大长度进行输入
    • 支持手机号、邮箱、普通文本等类型
  • 红点标记

    • 将要被点击的View会以红点标记保存为截图
    • 如果发现截图没有红点或者红点位置明显错误时,不用惊讶,那一定是隐藏的View被点击了
      • 没有红点:隐藏的View坐标不在屏幕范围
      • 红点标记错误:点击到被遮挡的View,通常发生在ViewPager布局
  • 无惧遮挡

    • 被遮挡的View也可以点击到,因此无需滑动操作。
    • 举例:列表一次性加载10条数据,屏幕只显示了5条,剩下5条没有显示的也可以点击到。
  • 完全遍历

    • 应用所有Activity都可以遍历到,360°无死角。
    • 快速模式、迭代模式覆盖度最高可达100%,通过爬虫模式 + 录制模式组合产生的Params.json文件,或单独录制模式产生的Params.json文件。
    • 爬虫模式亦可达到很高的覆盖度,不同应用覆盖度不一致,依赖Activity关联度。
      • 提高爬虫模式覆盖度的方法:采用划分模块的方法,比如主页模块、个人模块等等
  • 一触即达

    • 只需一步就能打开应用内任何Activity
    • 举例:在已经登录的情况下,想去到登录页面,一般可能的做法是在主页点击我,去到个人中心,个人中心滑动到最底部,点击退出登录,来到登录页面。一触即达只要知道登录页面的名称、启动参数就能直接打开登录页面。
  • 可复现性

    • 在数据相对不变的情况下,遍历Activity中View的顺序是一致的,因此具备一定的复现可能性,可理解为Monkey中的种子
  • 多重跟踪

    • 多重跟踪能在出现崩溃的情况下,更好的定位、复现、分析问题。
    • 截图跟踪:每个点击操作都将被记录,根据截图顺序可知进行了何等操作
    • 日志跟踪:崩溃日志抓取,供研发使用
    • 接口跟踪:配合Fiddler等抓包工具,可知发生崩溃时请求了哪些接口,从而更好的定位问题
    • 元素跟踪:点击的View信息以操作日志形式记录在SD card,包含包名、类名、资源ID、屏幕位置、文本等等信息
  • 支持Hybrid

    • 除了支持Native遍历,亦支持Hybrid

技术细节(局部)

关于跳转的处理

  1. 每次点击后都会判断是否离开遍历Activity(未离开则进入下一个点击事件)

  2. 如果跳转到本应用其他Activity(则按下返回键返回,返回后回不到遍历Activity则重启该Activity并重新遍历剩余View)

  3. 如果跳转到其他应用去了(如相机)则直接重启该Activity并重新遍历剩余View

  4. 如果跳转到登录页面则登录后继续操作(可能存在遍历时点击到退出登录按钮)

关于直接启动Activity的处理

通过监听Activity的启动,拿到Activity实例并获取传入参数,看下流程图可能好理解:

配置说明

参数描述:

// Activity截图开关,默认为true。启动Activity首先会截取一张图保存在sdcard/AutoClick/Screenshots/Activities文件夹
public boolean activityScreenShots = true;
// Activity迭代截图开关,默认为true。每次点击View会截取一张图保存在sdcard/AutoClick/Screenshots/对应Activity文件夹
public boolean iterationScreenShots = true;
/**
         * 迭代模式
         快速模式:只启动Activity,快速检测崩溃问题(如兼容性、混淆、代码问题导致的崩溃),一般几分钟可完成。依赖于Params.json文件,该文件可由录制模式产生。
         迭代模式:启动Activity并点击每个View。依赖于Params.json文件,该文件可由录制模式产生。
         爬虫模式:通过迭代主页并记录新开Activity,迭代完毕后读取新开Activity,循环往复,直至无新的Activity。
         录制模式:需人工操作应用,记录每个新开的Activity,供快速模式、迭代模式使用。录制模式可在功能测试阶段使用,录制模式默认休眠1个小时,期间操作应用打开的Activity都将被记录下来。
         */
public Mode mode;
// 被测应用主页,必填项
public String homeActivity;
// 被测应用登录页,必填项
public String loginActivity;
// 被测应用登录账户,必填项
public String loginAccount;
// 被测应用登录密码,必填项
public String loginPassword;
// 被测应用登录页面登录按钮资源ID,必填项
public String loginId;
// 被测应用包名,必填项
public String PACKAGE;
// 忽略的Activities数组,此数组内的Activity不会遍历
public String[] ignoreActivities;
// 忽略的Views数组,此数组内的View不会遍历,需填写完整的资源ID,如com.xx:id/iv_fpc_back
public String[] ignoreViews;
// Activities截图保留开关,默认为true,如果为false,Activity遍历完成后,截图将会被清理,Activity发生崩溃时,截图不会被清理。
public boolean keepActivitiesScreenShots = true;

示例:

package application.iteration;

import android.test.ActivityInstrumentationTestCase2;

import com.robotium.solo.Solo;

import org.junit.After;
import org.junit.Before;


@SuppressWarnings({"rawtypes", "deprecation"})
public class Iteration extends ActivityInstrumentationTestCase2 {

    /**
     * 被测应用包名
     */
    private static final String PACKAGE = "被测应用包名";

    /**
     * 被测应用Activity入口
     */
    private static final String LAUNCHER_ACTIVITY = "被测应用Activity入口";

    private static Class<?> launcherActivityClass;

    static {
        try {
            launcherActivityClass = Class.forName(LAUNCHER_ACTIVITY);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings("unchecked")
    public Iteration() {
        super(PACKAGE, launcherActivityClass);
    }

    private Solo solo;

    @Before
    public void setUp() throws Exception {
        Solo.Config config = new Solo.Config();
        // 遍历模式
        config.mode = Solo.Config.Mode.REPTILE;
        config.homeActivity = "被测应用主页Activity";
        config.loginActivity = "被测应用登录Activity";
        config.loginAccount = "登录帐号";
        config.loginPassword = "登录密码";
        config.loginId = "登录按钮ID";
        // 被测应用包名
        config.PACKAGE = PACKAGE;
        config.ignoreActivities = new String[]{"忽略的Activity,此数组中的Activity将不会被遍历"};
        config.ignoreViews = new String[]{"忽略的View,此数组中的View将不会被点击,需填入完整的资源ID"};

        solo = new Solo(getInstrumentation(), config, getActivity());
        super.setUp();
    }

    @After
    public void tearDown() throws Exception {
        solo.finishOpenedActivities();
        super.tearDown();
    }


    /**
     * 自动遍历入口
     * @throws Exception 抛出异常
     */
    public void test_iteration() throws Exception {
        solo.startIteration();
    }

}

AndroidManifest.xml配置

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="被测应用包名.test"
    android:versionCode="1"
    android:versionName="1.0">

    <uses-permission android:name="android.permission.GET_TASKS" />

    <uses-sdk 
        android:minSdkVersion="18"
        android:targetSdkVersion="24" />

    <instrumentation
        android:name="android.test.InstrumentationTestRunner"
        android:targetPackage="被测应用包名" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <uses-library android:name="android.test.runner" />
    </application>

</manifest>

怎么运行

相关配置都到位了只要运行test_iteration()方法即可。

相关截图

跨应用

智能输入

红点标记

一触即达

FAQ

Q: 跳转到其他应用回不去怎么办?

A:可能存在机型兼容问题,如果遇到该情况可以把该View加入忽略数组。

Q:遍历时出现object not found怎么办?

A:Object文件是记录类似序列化的传入参数,记录在sdcard/AutoClick/Object/目录下,务必保证它的存在。

Q:迭代模式下,一会就退出了,并没有遍历?

A:请检查sdcard/AutoClick/Params.json是否存在,或者该文件没有数据?

Q:自动遍历启动不了是什么情况?

A:请根据错误日志检查是否配置文件缺少必备参数,或者签名不一致?

Q:程序中途终止了?

A:确保数据线是连接状态,遍历需要用到adb

Q:Android 6.0及以上版本时,卡在授权界面?

A:如果第三方厂商更改过底层代码,可能出现兼容性问题(如小米),此时需要在Permission.java类中增加相应的包名及授权资源id,通过uiautomatorviewer查看授权界面信息。

Github

AutoClick

结语

  • 欢迎提供指导性意见,大家一同探讨学习
  • 由于水平有限,文中如有错误,请轻拍
  • 代码已经开源,如果觉得对你有帮助,可以star
  • 文章下方有打赏按钮,你的支持是我最大的动力
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容