Robotium自动遍历方案

本帖已被设为精华帖!,

背景

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

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

思路

  • 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的遍历

贴一张图

Robotium自动遍历方案

目前一些已实现的节点

  • 获取项目所有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

讨论区

  • 欢迎提出指导性意见
  • 致力于解放双手
  • 文中如有错误,请轻拍

* 注:本文来自网络投稿,不代表本站立场,如若侵犯版权,请及时知会删除