这是我入职新公司以来第一个相对来说比较成型的工具,虽然功能是那么的弱智,但是基本上我是抱着认真的态度来看待这个工具的开发
废话不多说,首先阐明一下这个工具的意图:
意图:起因是当时需要测试公司APK的稳定性,开发建议使用Monkey,但是Monkey是有很多弊病的,比如加-p参数即使加了指定包名,也还是会有时跳出被测程序,跑到OS里去执行;还比如测试中经常会有需要模拟按键的操作,比如音量,HOME之类的,这些是我所不需要的,而恰恰公司4个APK中都有的左滑右滑貌似没有支持,所以萌生出了一个自己用robotium写一个类似于Monkey操作的脚本
解释一下为什么我会选择使用坐标点击,而不是使用控件集来进行随机点击,我公司有一个业务逻辑很复杂,界面很乱的手机助手APK,起初我使用getCurrentViews()方法尝试过对控件进行筛选,然后随机点击,但是由于各种空指针,而且由于界面布局上控件过于繁杂,在获取上的效率非常之慢;但是这个方法在我公司中另一个界面比较规范简洁的APK上测试,确实会比坐标点击的有效率高很多,综合考虑通用性以及稳定性,操作性各个方面,最终我还是敲定使用坐标随机这种方式进行实现
这篇博文我会持续更新,按照我当时开发工具的顺序进行讲解,其中涉及到一些android开发相关的东西,所以我会一点点把整个工具的开发思路,代码都顺序写下来,也让大伙方便理解和思考
一、让Monkey跑起来
原理:要实现Monkey操作其实特别简单,但是这里有一个可以扩展的地方,就是,我们怎么让脚本,可以适配各种屏幕尺寸呢,所以具体思路就是:我们要在点击之前,使用一个方法去获取到当前屏幕的宽和高,然后分别使用这个宽和高利用随机数函数生成随机值,然后进行随机坐标点击;还有一个问题,取得屏幕的宽高,是会将上方状态栏,也就是信号栏那一条的坐标算进去,点击那里可是会弹出通知中心的,那样我们的脚本不就挂了吗,所以我们还需要一个方法去计算状态栏的宽度,然后去计算,代码如下:
public class BaihMonkey extends ActivityInstrumentationTestCase2 {
public static String LAUNCHER_ACTIVITY_FULL_CLASSNAME="com.android.haoyouduo.StartupActivity" ;
private static Class launcherActivityClass;
DisplayMetrics ty=new DisplayMetrics();
//静态加载将获取到的点击的MainActivity字符串读出来
// static{
// File file=new File("/mnt/sdcard", "activityName.txt");
//
// try {
// BufferedReader fileReader = new BufferedReader (new FileReader(file));
// String activityName = fileReader.readLine();
// System.out.println(activityName);
// LAUNCHER_ACTIVITY_FULL_CLASSNAME=activityName;
// fileReader.close();
// } catch (FileNotFoundException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// } catch (IOException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
//
// }
public BaihMonkey() throws ClassNotFoundException {
super(Class.forName(LAUNCHER_ACTIVITY_FULL_CLASSNAME));
}
private Solo solo;
String logtag="LikeMonkey_log";
@Override
protected void setUp() throws Exception {
solo = new Solo(getInstrumentation(), getActivity());
}
public void testMonkey() throws InterruptedException{
Thread.sleep(6000);
while(true)
{
Thread.sleep(2000);
//特殊操作随机触发机制
Random setindex=new Random();
int setId=setindex.nextInt(20);
Log.e(logtag, "特殊操作值:"+setId);
switch (setId) {
case 2:
Log.e(logtag, "操作左滑动");
solo.scrollToSide(solo.LEFT, (float) 0.8); //左滑动
break;
case 5:
Log.e(logtag, "操作右滑动");
solo.scrollToSide(solo.RIGHT, (float) 0.8); //右滑动
break;
case 10:
Log.e(logtag, "操作返回");
solo.goBack(); //返回
break;
}
int ClickX=createX();
int ClickY=createY();
Log.e("baih", "x="+ClickX);
Log.e("baih", "y="+ClickY);
//随机屏幕坐标点击机制(去除信号栏高度)
if(ClickX>=ty.widthPixels || ClickY>=ty.heightPixels)
{
continue;
}
else
{
Log.e(logtag, "点击坐标为:x="+ClickX+" y="+ClickY);
solo.clickOnScreen(ClickX, ClickY);
}
}
}
//获取屏幕X轴长度并计算X轴随机点
public int createX(){
solo.getCurrentActivity().getWindowManager().getDefaultDisplay().getMetrics(ty);
int x1=ty.widthPixels;
Random x=new Random();
int Rxindex=x.nextInt(x1);
int xIndex=Rxindex+10;
return xIndex;
}
//获取屏幕Y轴长度(去除信号栏高度)并计算Y轴随机点
public int createY(){
Rect frame=new Rect();
solo.getCurrentActivity().getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
int statusBarHeight=frame.top;//计算顶部信号栏高度
solo.getCurrentActivity().getWindowManager().getDefaultDisplay().getMetrics(ty);
int y1=ty.heightPixels;
Random y=new Random();
int Ryindex=y.nextInt(y1);
int yIndex=Ryindex+statusBarHeight+5;
return yIndex;
}
@Override
public void tearDown() throws Exception {
solo.finishOpenedActivities();
}
}
注释已经将各功能的实现写的很明白了,通过使用DisplayMetrics对象的widthPixels和heightPixels方法,我们可以得到当前设备的宽高(设备分辨率还需要考虑DPI的值,此处我没有考虑进去,因为还不知道分辨率和颗粒密度之间如何计算,这个后期准备研究下)
二、让脚本封装成APK
这个在我前一篇随笔里面有比较详细的记录,这里不再多说,各位可以自行去研究,或者在基础上改良
三、Activity跳转了怎么办?
在实际测试中发现,我们公司的一款手机应用市场APK,在下载完成一个应用后,会自动弹出系统的程序安装界面,在点击一个已安装的应用时,也会自动弹出系统的程序卸载界面,这样的Acticity切换会导致我的脚本因为活动进程不在被测程序中而挂掉,也就又回归到了2个月前我用appium写LikeMonkey的问题:怎么可以启动一个线程去实时监听Activity的切换,并且还不影响主线程(即操作线程)的执行,这个时候,我想到了android四大基本组件里的Service,关于service的概念,各位可以自行百度
我在测试工具启动时,在界面onCreate中启动一个service,这个Service的onCreate中去另启一个线程循环去监听当前的Activity栈的最顶部Activity,如果检测到当前最顶部的Activity是系统的安装界面或者卸载界面,就startActivity唤醒我的被测程序,代码如下:
//该类继承Service,实现实时监听
public class StartService extends Service {
public static String activityName;
public boolean setWhile=true;
@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return null;
}
public void onCreate(){
Log.e("baih", "进入了onCreate里面");
IntentFilter intent =new IntentFilter("android.intent.action.VIEW");
//intent.addAction(Intent.ACTION_VIEW);
intent.setPriority(Integer.MAX_VALUE);
Toast.makeText(getApplicationContext(), "service已启动", 3000).show();
Log.e("baih", "===================service已启动");
//从Activity栈中获取当前系统Activity列表
final ActivityManager ActivityList=(ActivityManager)getApplicationContext().getSystemService(ACTIVITY_SERVICE);
//另启线程,完成监听安装界面弹出工作
new Thread(){
public void run(){
while(setWhile)
{
//从Activity列表中读取一个RunningTaskInfo
List<RunningTaskInfo> acList=ActivityList.getRunningTasks(1);
//得到第一个RunningTaskInfo
RunningTaskInfo mTaskInfo;
mTaskInfo=acList.get(0);
//获取该RunningTaskInfo的ActivityName
String name=mTaskInfo.topActivity.getClassName();
String setup="com.android.packageinstaller.PackageInstallerActivity";
String uninstall="com.android.packageinstaller.UninstallerActivity";
//判断获取到的ActivityName是否为系统的安装界面或者卸载界面
if(name.equals(setup) || name.equals(uninstall) )
{
Log.e("baih", "已经跳转到安装/卸载界面,准备操作返回随乐游");
//从Acitivity列表中读取两个RunningTaskInfo
List<RunningTaskInfo> ac1=ActivityList.getRunningTasks(2);
//得到第二个RunningTaskInfo
RunningTaskInfo ra1;
ra1=ac1.get(1);
//获取该RunningTaskInfo的ActivityName
String name1=ra1.topActivity.getClassName();
ComponentName componentName = ra1.topActivity;
//启动新Activity指向到被测程序
Intent intent = new Intent();
//intent.setComponent(componentName);
intent.setClassName("com.stnts.suileyoo.gamecenter", "com.android.haoyouduo.StartupActivity");
intent.setAction(Intent.ACTION_MAIN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Log.e("baih", "===========操作返回");
Log.e("baih", "==========="+name1);
}
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
}.start();
}
public void onStart(){
Log.e("baih", "进入了onStart里面");
}
public void onDestroy(){
setWhile=false;
}
}
//这个类实现测试工具启动的Activity
package test.Monkey;
import java.io.IOException;
import test.Monkey.R;
import test.Monkey.*;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
public class Start extends android.app.Activity{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button btn1=(Button) findViewById(R.id.startTest);
btn1.setOnClickListener(my);
//启动Service
Intent serviceIntent =new Intent(this,StartService.class);
startService(serviceIntent);
LogOutput log=LogOutput.getInstance();
log.startLog();
}
public void onDestroy(){
//在关闭测试工具的时候关闭Service
super.onDestroy();
LogOutput log=LogOutput.getInstance();
log.stopLog();
Intent intent1=new Intent(this,StartService.class);
Toast.makeText(getApplicationContext(), "service已关闭", 3000).show();
stopService(intent1);
}
private OnClickListener my=new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Log.e("baih", "====================================");
//使用命令行启动测试脚本
Runtime run=Runtime.getRuntime();
try {
run.exec("am instrument -w test.Monkey/test.Monkey.InstrumentationTestRunner");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
}
四、怎么输出Log,怎么让开发人员debug
monkey测试这类工作,基本都是不需要人员看护,自己进行脚本执行的,所以我们就面对一个问题,出了问题,没人看见怎么办,所以我们需要一个功能,可以在脚本运行的过程中,把程序执行的logcat输出到本地,这个位置的功能不多说,直接上代码:
//这个类的作用是输出Log到手机存储空间根目录下
public class LogOutput {
private static final String TAG="Log";
private String LOG_PATH;
private SimpleDateFormat time=new SimpleDateFormat("yyyy-mm-dd-HH-mm-ss");
private Process pro;
private static LogOutput Logfile=null;
private LogOutput(){
init();
}
public static LogOutput getInstance(){
if(Logfile==null)
{
Logfile=new LogOutput();
}
return Logfile;
}
public void startLog(){
createLog();
}
public void stopLog(){
if(pro!=null)
{
pro.destroy();
}
}
private void init(){
LOG_PATH=Environment.getExternalStorageDirectory().getAbsolutePath();
createLogDir();
Log.e(TAG, "Log onCreate");
}
public void createLog(){
List<String> commandlist=new ArrayList<String>();
commandlist.add("logcat");
commandlist.add("-f");
commandlist.add(getLogPath());
commandlist.add("-s");
commandlist.add("*:E");
commandlist.add("-v");
commandlist.add("time");
try {
pro=Runtime.getRuntime().exec(commandlist.toArray(new String[commandlist.size()]));
} catch (Exception e) {
// TODO: handle exception
Log.e(TAG,e.getMessage(), e);
}
}
public String getLogPath(){
createLogDir();
String logFileName=time.format(new Date())+"_suileyoo_LikeMonkey.log";
return LOG_PATH+File.separator+logFileName;
}
private void createLogDir(){
File file;
boolean OK;
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
{
file=new File(LOG_PATH);
if(!file.isDirectory()){
OK=file.mkdirs();
if(!OK)
{
return;
}
}
}
}
}
编写好Log输出类时,我们只需要在程序启动时调用开始打印log的函数
LogOutput log=LogOutput.getInstance();
log.startLog();
在程序关闭时调用停止打印log的函数
LogOutput log=LogOutput.getInstance();
log.stopLog();
并且在工程的manifest文件中添加读取系统log的权限
<uses-permission android:name="android.permission.READ_LOGS"/>
如此,一个可以适配各种屏幕尺寸,可以输出log到本地的Monkey脚本,就基本成型了