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

前言

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

解决了什么

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

使用效果

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

崩溃问题:

特性

技术细节(局部)

关于跳转的处理

  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

交流群

结语

打赏支持


↙↙↙阅读原文可查看相关链接,并与作者交流