Robotium Robotium 自动遍历方案

Heyniu · 2016年09月20日 · 3420 次阅读
本帖已被设为精华帖!

背景

项目经常遇到混淆打包、安卓版本兼容带来的崩溃问题,还有少部分代码引起的崩溃问题。

人工验证即费时也枯燥,故琢磨有没办法可以快速的验证这些问题,最重要的是快、最重要的是快、最重要的是快

思路

  • robotium 采用 Instrumentation 进行二次封装,Instrumentation 与被测应用属于同一进程,故 robotium 可以做一些想做的事情,比如启动一个 Activity
  • ios 怎么办?目前我也没思路,希望知道的可以告诉我(Appium 太慢了,先不考虑)

验证点

目前只验证崩溃问题,而且初步只是验证启动 Activity 的崩溃问题,后续增加自动遍历页面所有控件的方法,亦可考虑增加 UI 版本 diff 对比

实现了吗?

实现了,亲测有效,只是还没系统的整合在一起。

只要有 Activity 挂了,robotium 就会挂,检测到挂了就导出日志

怎么检测有没挂?

  • python 通过 adb 不定时监控目标应用是否在运行,如果未运行,那么导出日志,查找有没目标应用崩溃日志,有的话,那么肯定挂了
  • 挂了的话,再判断所有页面都遍历完成?没有的话再组装数据,进行下一轮测试

Activity 需要传参才能打开的怎么办?

有办法,源码传什么参数,robotium 就传什么参数过去

无参数的情况

Class <?> LoginClass;
LoginClass = Class.forName("Activity路径");
Intent intent = new Intent(getActivity(), LoginClass);
getActivity().startActivity(intent);

有参数的情况

Class <?> GroupMemberClass;
GroupMemberClass = Class.forName("Activity路径");
Intent intent = new Intent(getActivity(), GroupMemberClass);
intent.putExtra("GroupId", 77);
getActivity().startActivity(intent);

传参怎么管理?

配置文件以 Activity 为节点,通过字典的形式配置好,一劳永逸,后面只需少量改动,如页面变动等等

能有多快?

预测 5 分钟内可完成 Activity 的遍历

贴一张图

目前一些已实现的节点

  • 获取项目所有 Activities
  • 重签名
  • 本项目常用 adb 工具(后面补全,当做一个模块用)
#!/usr/bin/evn python
# -*- coding:utf-8 -*-

# FileName activities.py
# Author: HeyNiu
# Created Time: 2016/9/18
"""
获取项目所有Activity
"""


import re
import os
import utils.adbtools
import utils.consts


class Activities(object):

    def __init__(self, apk_path, manifest_path):
        """
        初始化
        :param apk_path: apk文件路径
        :param manifest_path: AndroidManifest.xml 路径
        """
        self.apk_path = apk_path
        self.manifest_path = manifest_path
        self.dump_stream = utils.adbtools.AdbTools().dump_apk(apk_path).readlines()
        self.manifest_stream = self.__read_file()
        self.__init_data()

    def __init_data(self):
        """
        初始化apk基本信息
        :return:
        """
        for i in self.dump_stream:
            if 'package' in i:
                reg = re.compile("name='(.+?)'")
                utils.consts.PACKAGE = re.findall(reg, i)[0]  # 包名
                reg = re.compile("versionCode='(.+?)'")
                utils.consts.VERSION_CODE = re.findall(reg, i)[0]  # build版本
                reg = re.compile("versionName='(.+?)'")
                utils.consts.VERSION_NAME = re.findall(reg, i)[0]  # 版本号
            if 'launchable-activity' in i:
                reg = re.compile("name='(.+?)'")
                utils.consts.LAUNCHER_ACTIVITY = re.findall(reg, i)[0]  # 启动Activity

    def __read_file(self):
        """
        读取AndroidManifest.xml
        :return:
        """
        if os.path.exists(self.manifest_path):
            return open(self.manifest_path, encoding='utf-8').read()
        raise FileNotFoundError('AndroidManifest.xml not found.')

    def __match_activities(self):
        """
        匹配出所有Activity
        :return:
        """
        reg = re.compile("<activity\s(.*)")
        l = re.findall(reg, self.manifest_stream)
        regex = re.compile(r'android:name="(.+?)"')
        return ('%s/%s%s' % (utils.consts.PACKAGE, utils.consts.PACKAGE, re.findall(regex, i)[0]) for i in l)

    def ignore__activities(self, ignore_activities):
        """
        排除忽略的Activities
        :return:
        """
        all_activities = self.__match_activities()
        activities = list(all_activities)
        for activity in activities:
            for i in ignore_activities:
                if i in activity:
                    activities.remove(activity)
        return activities


if __name__ == '__main__':
    pass

#!/usr/bin/evn python
# -*- coding:utf-8 -*-

# FileName resign.py
# Author: HeyNiu
# Created Time: 2016/9/19
"""
重签名apk
"""

import os
import utils.errors


class Resign(object):

    def __init__(self, path):
        """
        初始化
        :param path: apk文件路径
        """
        self.path = path
        self.__check_environment()
        apk_path = self.__check_apk_path()
        self.oldapk = apk_path
        self.newapk = apk_path[::-1].split('.', 1)[1][::-1] + '_debug.apk'

    @staticmethod
    def __check_environment():
        """
        环境变量检测
        :return:
        """
        if "ANDROID_HOME" not in os.environ:
            raise EnvironmentError("ANDROID_HOME PATH NOT FOUND.\nPlease set the environment variable.")

        if "7Z_HOME" not in os.environ:
            raise EnvironmentError("7Z_HOME PATH NOT FOUND.\nPlease set the environment variable.\n"
                                   "Don't installed 7-zip? click the url download.\n"
                                   "http://www.7-zip.org/")

        # 检查build-tools是否添加到环境变量中
        # 需要用到里面的zipalign命令
        l = os.environ['PATH'].split(';')
        build_tools = False
        for i in l:
            if 'build-tools' in i:
                build_tools = True
        if not build_tools:
            raise EnvironmentError("ANDROID_HOME BUILD-TOOLS COMMAND NOT FOUND.\nPlease set the environment variable.")

    def __check_apk_path(self):
        """
        检查path是否合法apk
        :return:
        """
        if not self.path.endswith('.apk'):
            raise utils.errors.InvalidApkFile('无效apk文件! %s' % (self.path,))
        return self.path

    def __make_file(self):
        """
        apk文件改名zip,并处理掉原签名
        :return:
        """
        temp = self.oldapk[::-1].split('.', 1)[1][::-1].split('\\')[-1].split()[0]
        zip_filename = '%s.zip' % (temp,)
        apk_filename = '%s.apk' % (temp,)
        zip_path = os.path.join(self.oldapk[::-1].split('\\', 1)[-1][::-1], zip_filename)
        os.system('ren %s %s' % (self.oldapk.split()[0], zip_filename))
        os.system('7z d %s META-INF' % zip_path)
        os.system('ren %s %s' % (zip_path, apk_filename))

    def resign(self):
        """
        重签名apk
        :return:
        """
        self.__make_file()
        local_keystore = os.path.join(os.path.expanduser('~'), '.android\debug.keystore')
        if not os.path.exists(local_keystore):
            raise utils.errors.KeyStoreNotFound('请确保签名文件存在%s' % (local_keystore,))
        os.system('jarsigner -digestalg SHA1 -sigalg MD5withRSA -keystore %s\.android\debug.keystore -storepass android '
                  '-keypass android %s androiddebugkey' % (os.path.expanduser('~'), self.oldapk))
        os.system('zipalign 4 %s %s' % (self.oldapk, self.newapk))

if __name__ == '__main__':
    pass


#!/usr/bin/evn python
# -*- coding:utf-8 -*-

# FileName adbtools.py
# Author: HeyNiu
# Created Time: 2016/9/19
"""
adb 工具类
"""

import os
import platform
import re
import time


class AdbTools(object):

    def __init__(self, device_id=''):
        self.system = platform.system()
        self.find = ''
        self.command = ''
        self.device_id = device_id
        self.__get_find()
        self.__check_adb()
        self.__connection_devices()

    def __get_find(self):
        """
        判断系统类型,windows使用findstr,linux使用grep
        :return:
        """
        if self.system is "Windows":
            self.find = "findstr"
        else:
            self.find = "grep"

    def __check_adb(self):
        """
        检查adb
        判断是否设置环境变量ANDROID_HOME
        :return:
        """
        if "ANDROID_HOME" in os.environ:
            if self.system == "Windows":
                path = os.path.join(os.environ["ANDROID_HOME"], "platform-tools", "adb.exe")
                if os.path.exists(path):
                    self.command = path
                else:
                    raise EnvironmentError(
                        "Adb not found in $ANDROID_HOME path: %s." % os.environ["ANDROID_HOME"])
            else:
                path = os.path.join(os.environ["ANDROID_HOME"], "platform-tools", "adb")
                if os.path.exists(path):
                    self.command = path
                else:
                    raise EnvironmentError(
                        "Adb not found in $ANDROID_HOME path: %s." % os.environ["ANDROID_HOME"])
        else:
            raise EnvironmentError(
                "Adb not found in $ANDROID_HOME path: %s." % os.environ["ANDROID_HOME"])

    def __connection_devices(self):
        """
        连接指定设备,单个设备可不传device_id
        :return:
        """
        if self.device_id == "":
            return
        self.device_id = "-s %s" % self.device_id

    def adb(self, args):
        """
        执行adb命令
        :param args:参数
        :return:
        """
        cmd = "%s %s %s" % (self.command, self.device_id, str(args))
        return os.popen(cmd)

    def shell(self, args):
        """
        执行adb shell命令
        :param args:参数
        :return:
        """
        cmd = "%s %s shell %s" % (self.command, self.device_id, str(args))
        return os.popen(cmd)

    def get_devices(self):
        """
        获取设备列表
        :return:
        """
        l = self.adb('devices').readlines()
        return (i.split()[0] for i in l if 'devices' not in i and len(i) > 5)

    def get_package(self):
        """
        获取当前运行app包名
        :return:
        """
        result = self.shell('dumpsys window w | %s \/ | %s name=' % (self.find, self.find)).read()
        reg = re.compile(r'name=(.+?)/')
        return re.findall(reg, result)[0]

    def get_pid(self, package_name):
        """
        获取pid
        :return:
        """
        if self.system is "Windows":
            pid_command = self.shell("ps | %s %s$" % (self.find, package_name)).read()
        else:
            pid_command = self.shell("ps | %s -w %s" % (self.find, package_name)).read()

        if pid_command == '':
            return "the process doesn't exist."

        req = re.compile(r"\d+")
        result = str(pid_command).split()
        result.remove(result[0])
        return req.findall(" ".join(result))[0]

    def get_uid(self, pid):
        """
        获取uid
        :param pid:
        :return:
        """
        result = self.shell("cat /proc/%s/status" % pid).readlines()
        for i in result:
            if 'uid' in i.lower():
                return i.split()[1]

    @staticmethod
    def dump_apk(path):
        """
        dump apk文件
        :param path: apk路径
        :return:
        """
        # 检查build-tools是否添加到环境变量中
        # 需要用到里面的aapt命令
        l = os.environ['PATH'].split(';')
        build_tools = False
        for i in l:
            if 'build-tools' in i:
                build_tools = True
        if not build_tools:
            raise EnvironmentError("ANDROID_HOME BUILD-TOOLS COMMAND NOT FOUND.\nPlease set the environment variable.")
        return os.popen('aapt dump badging %s' % (path,))

    def uiautomator_dump(self):
        """
        获取屏幕uiautomator xml文件
        :return:
        """
        return self.shell('uiautomator dump').read().split()[-1]

    def pull(self, source, target):
        """
        从手机端拉取文件到电脑端
        :return:
        """
        self.adb('pull %s %s' % (source, target))

    def remove(self, path):
        """
        从手机端删除文件
        :return:
        """
        self.shell('rm %s' % (path,))

    def clear_app_data(self, package):
        """
        清理应用数据
        :return:
        """
        self.shell('pm clear %s' % (package,))

    def install(self, path):
        """
        安装apk文件
        :return:
        """
        # adb install 安装错误常见列表
        errors = {'INSTALL_FAILED_ALREADY_EXISTS': '程序已经存在',
                  'INSTALL_FAILED_INVALID_APK': '无效的APK',
                  'INSTALL_FAILED_INVALID_URI': '无效的链接',
                  'INSTALL_FAILED_INSUFFICIENT_STORAGE': '没有足够的存储空间',
                  'INSTALL_FAILED_DUPLICATE_PACKAGE': '已存在同名程序',
                  'INSTALL_FAILED_NO_SHARED_USER': '要求的共享用户不存在',
                  'INSTALL_FAILED_UPDATE_INCOMPATIBLE': '版本不能共存',
                  'INSTALL_FAILED_SHARED_USER_INCOMPATIBLE': '需求的共享用户签名错误',
                  'INSTALL_FAILED_MISSING_SHARED_LIBRARY': '需求的共享库已丢失',
                  'INSTALL_FAILED_REPLACE_COULDNT_DELETE': '需求的共享库无效',
                  'INSTALL_FAILED_DEXOPT': 'dex优化验证失败',
                  'INSTALL_FAILED_OLDER_SDK': '系统版本过旧',
                  'INSTALL_FAILED_CONFLICTING_PROVIDER': '存在同名的内容提供者',
                  'INSTALL_FAILED_NEWER_SDK': '系统版本过新',
                  'INSTALL_FAILED_TEST_ONLY': '调用者不被允许测试的测试程序',
                  'INSTALL_FAILED_CPU_ABI_INCOMPATIBLE': '包含的本机代码不兼容',
                  'CPU_ABIINSTALL_FAILED_MISSING_FEATURE': '使用了一个无效的特性',
                  'INSTALL_FAILED_CONTAINER_ERROR': 'SD卡访问失败',
                  'INSTALL_FAILED_INVALID_INSTALL_LOCATION': '无效的安装路径',
                  'INSTALL_FAILED_MEDIA_UNAVAILABLE': 'SD卡不存在',
                  'INSTALL_FAILED_INTERNAL_ERROR': '系统问题导致安装失败',
                  'DEFAULT': '未知错误'
                  }
        print('Installing...')
        l = self.adb('install %s' % (path,)).read()
        if 'Success' in l:
            print('Install Success')
        if 'Failure' in l:
            reg = re.compile('\\[(.+?)\\]')
            key = re.findall(reg, l)[0]
            print('Install Failure >> %s' % (errors[key],))

    def uninstall(self, package):
        """
        卸载apk
        :param package: 包名
        :return:
        """
        print('Uninstalling...')
        l = self.adb('uninstall %s' % (package,)).read()
        print(l)

    def screenshot(self, target_path=''):
        """
        手机截图
        :param target_path: 目标路径
        :return:
        """
        format_time = self.timestamp('%Y%m%d%H%M%S')
        self.shell('screencap -p /sdcard/%s.png' % (format_time,))
        time.sleep(1)
        if target_path == '':
            self.adb('pull /sdcard/%s.png %s' % (format_time, os.path.expanduser('~')))
        else:
            self.adb('pull /sdcard/%s.png %s' % (format_time, target_path))
        self.shell('rm /sdcard/%s.png' % (format_time,))

    @staticmethod
    def timestamp(format_time):
        """
        获取当前时间
        :return:
        """
        return time.strftime(format_time, time.localtime(time.time()))


if __name__ == '__main__':
    pass


大家关心的重点来了

开源吗?

当然开源,后面完成后,我会上传到 github

讨论区

  • 欢迎提出指导性意见
  • 致力于解放双手
  • 文中如有错误,请轻拍
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 22 条回复 时间 点赞

高产呀😂

“使劲”“用力” 的顶起来,黑妞这更新速度真不是盖的

#2 楼 @thinker 杠杠地

Heyniu #20 · 2016年09月20日 Author

#1 楼 @sycing 一起加油,解放双手吧😆

#4 楼 @heyniu 解放的双手别撸太多,对身体不好,嘿嘿

#5 楼 @darker50 嘿嘿,太污了

出现崩溃?这个两边的 YES 或 NO 是不是反了

Heyniu #10 · 2016年09月20日 Author

#7 楼 @yuweixx 没有反,思路没排版好,造成的错觉,这图我自己都嫌弃,嘿嘿

adb 的调用批处理这段调整下吧。可以看看https://github.com/pdhxxj/HATT 里面的处理

代码贴的有点多啊😅

@heyniu 是单 Activity 的 UI 遍历吧?

Heyniu #12 · 2016年09月20日 Author

#9 楼 @kasi 有空看看😄

Heyniu #15 · 2016年09月20日 Author

#10 楼 @dongdong 有用就好

Heyniu #10 · 2016年09月20日 Author

#11 楼 @tediwang 应用所有页面的遍历,还没集成好

谢谢楼主分享,我想知道楼主的工作是在写工具吗?平常工作忙不,高产这么多,还是本身就这么厉害,随手就能写出这么多厉害的东西😂 ,好佩服,,有没有 github,勾搭下

Heyniu #18 · 2016年09月21日 Author

#15 楼 @lose 平时工作就是点点点啊

思寒_seveniruby 将本帖设为了精华贴 09月21日 13:23

加精理由: 实用 开源 设计精巧

请教楼主一个问题,我最近也在用 Robotium 做项目的自动化,但是基于 apk 遇到一个问题(已经重签名),但是运行测试的时候,提示 class not found, main activity 那个类找不到?看了一些帖子,无解。请问有可能是什么原因尼?

Heyniu #20 · 2016年09月21日 Author

#19 楼 @loupman main activity 是哪个?launcher activity 那个吗

#20 楼 @heyniu 嗯,对的。基于源码的方式,是正常的。

天下武功,唯快不破!

匿名 #23 · 2016年10月10日

必须收藏一发

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册